Files
2026-08-27 21:09:14 +00:00

13 KiB
Raw Permalink Blame History

verify-baseline-static — triage guide

Static ISA scanner. Disassembles every instruction in .text of a baseline Bun binary and flags anything the baseline CPU can't decode. Catches -march leaks at compile time, before they SIGILL on a user's machine.

This file is for triaging CI failures. For architecture details see README.md and inline comments in src/main.rs / src/aarch64.rs.

This is a best-effort check, not a proof

A PASS here does not guarantee the binary is baseline-safe, and a FAIL does not guarantee a real bug. Treat it as a sensitive smoke detector, not an oracle. The emulator phase (scripts/verify-baseline.ts) is the complementary check — together they catch most things; neither alone is bulletproof.

Out of scope entirely (tool will never find these):

  • JIT-emitted code. JSC compiles JS/WASM to machine code at runtime; none of it exists in .text at scan time. If the JIT backend emits post- baseline instructions on a baseline CPU, this tool is blind to it. The emulator's --jit-stress path covers this.
  • Dynamically loaded code. N-API addons, FFI callees, dlopen'd shared libs. Scanner only reads the bun-profile binary.
  • Gate correctness. The tool does not verify that a CPUID gate actually checks the right bits. It trusts the allowlist. Feature ceilings catch the "code grew new features, gate wasn't updated" case, but a gate that was wrong from the start (checks AVX, uses AVX2) passes silently if the ceiling says [AVX, AVX2].

In scope but may miss:

  • x64 linear-sweep may desync on data-in-.text and skip real instructions that follow. Variable-length x86 encoding makes perfect code/data separation undecidable (README.md:53-59). aarch64 is more reliable (fixed-width words, $d mapping symbols mark data), but a missing mapping symbol can still hide a hit.
  • Instructions deliberately ignored (TZCNT/XGETBV on x64, hint-space PAC/BTI on aarch64) could theoretically be misused; we assume the compiler's idiom is the only one.

Can report false violations:

  • Data bytes in .text that happen to form a valid post-baseline encoding. Rare on ELF (LLVM puts tables in .rodata), common on Windows PE (MSVC inlines jump tables). See README.md:61-74.

When in doubt, the emulator is ground truth: qemu -cpu Nehalem and hit the code path. SIGILL = real bug. No SIGILL = either gated or a data-in-text false positive.

Which builds run this

See needsBaselineVerification() in .buildkite/ci.mjs:

Target Allowlist file
linux-x64, linux-x64-musl allowlist-x64.txt
windows-x64 allowlist-x64-windows.txt
linux-aarch64, linux-aarch64-musl, linux-aarch64-android allowlist-aarch64.txt

x64 baseline = Nehalem (-march=nehalem). aarch64 baseline = armv8-a+crc. Every x64 build is baseline (there is no separate -baseline variant).

Reproduce a CI failure locally

The scanner runs on the CI-built -profile artifact. You can't reproduce by building locally unless you build with the exact baseline toolchain. Download the artifact instead.

  1. Get <triplet>-profile.zip from the failing build's build-bun step (Artifacts tab in Buildkite). Triplets look like bun-linux-x64, bun-linux-aarch64-musl, bun-windows-x64.

  2. Build and run the scanner (host arch is irrelevant — the scanner reads the binary's headers, it doesn't execute it):

    cargo build --release --manifest-path scripts/verify-baseline-static/Cargo.toml
    
    # Linux x64 baseline
    ./scripts/verify-baseline-static/target/release/verify-baseline-static \
      --binary bun-linux-x64-profile/bun-profile \
      --allowlist scripts/verify-baseline-static/allowlist-x64.txt
    
    # Linux aarch64
    ./scripts/verify-baseline-static/target/release/verify-baseline-static \
      --binary bun-linux-aarch64-profile/bun-profile \
      --allowlist scripts/verify-baseline-static/allowlist-aarch64.txt
    
    # Windows x64 baseline (PDB auto-discovered at <binary>.pdb)
    ./scripts/verify-baseline-static/target/release/verify-baseline-static \
      --binary bun-windows-x64-profile/bun-profile.exe \
      --allowlist scripts/verify-baseline-static/allowlist-x64-windows.txt
    

Never scan the stripped release binary. It has no .symtab (ELF) / no .pdb (PE), so every hit becomes <no-symbol@addr> and nothing matches the allowlist.

Reading the output

VIOLATIONS (would SIGILL on Nehalem):

  _ZN7simdutf7haswell14implementation17some_new_functionEPKcm  [AVX, AVX2]  (42 insns)
    0x0000a1b2c3  Vpbroadcastb  (AVX2)
    0x0000a1b2d7  Vpshufb  (AVX)
    0x0000a1b2ee  Vpcmpeqb  (AVX)
    ... 39 more

ALLOWLISTED (suppressed, runtime-dispatched):
  ...
  -- 550 symbols, 18234 instructions total

STALE ALLOWLIST ENTRIES (no matching symbol found — remove these?):
  _ZN7simdutf7haswell14implementation13old_gone_funcEPKcm

SUMMARY:
  violations:  1 symbols, 42 instructions
  allowlisted: 550 symbols
  stale allowlist entries: 1
  FAIL
  • Violation line format: symbol [FEAT, ...] (N insns). Copy the symbol name exactly when allowlisting — it's compared post-canonicalization.
  • Feature names are iced-x86's CpuidFeature Debug names (x64) or the strings in src/aarch64.rs:44-54 (aarch64). They must match the allowlist brackets character-for-character.
  • STALE entries are informational, not an error. One allowlist covers both glibc and musl; a symbol LTO'd away on one libc shows STALE on the other.

Triage: is this an allowlist entry or a real bug?

The tool found post-baseline instructions in some symbol. Two possibilities:

A. Runtime-dispatched. The symbol only runs after a CPUID/HWCAP gate decides the CPU supports it. This is fine — allowlist it.

B. Not gated. A -march flag leaked into a translation unit that's always executed. Real bug, will SIGILL on baseline hardware. Fix the compile flags.

Deciding which

Identify the dependency. Demangle the symbol (c++filt, or recognize the prefix: _ZN7simdutf = simdutf, _ZN3bun + N_AVX2/N_SVE = Bun's Highway code, _RNv + memchr = Rust memchr, etc). Search the allowlist for that dependency — if neighbors are there under an existing # Gate: ... header, this is almost certainly (A).

Find the gate. Grep for the symbol name (unmangled) in the dependency's source. Trace up to the caller — there should be a CPUID check, a dispatcher table, an HWCAP test. Known patterns:

Dependency Gate Where
simdutf set_best() — CPUID first call, cached atomic ptr vendor/ or WebKit's bundled copy
Highway (Bun) HWY_DYNAMIC_DISPATCHhwy::SupportedTargets() src/jsc/bindings/highway_strings.cpp
BoringSSL OPENSSL_ia32cap_P global, set at init vendor/boringssl/crypto/cpu_intel.c
zstd ZSTD_cpuid() vendor/zstd/lib/common/cpu.h
libdeflate libdeflate_init_x86_cpu_features() / HWCAP_ASIMDDP vendor/libdeflate/lib/x86/cpu_features.c
Rust memchr is_x86_feature_detected!() (via lolhtml dep)
compiler-rt outline-atomics (aarch64) __aarch64_have_lse_atomics (= AT_HWCAP & HWCAP_ATOMICS) compiler-rt builtin

If no gate exists: (B). Usually a subbuild that picked up host -march=native instead of the pinned -march=nehalem / -mcpu=cortex-a53. Fix that dep's compile flags in scripts/build/deps/. Confirm with the emulator (the ground-truth check):

qemu-x86_64 -cpu Nehalem ./bun-profile <code path that hits it>   # x64 → SIGILL = bug
qemu-aarch64 -cpu cortex-a53 ./bun-profile <code path>             # aarch64

Data-in-.text false positives (x64, mostly Windows)

Linear-sweep decode means data bytes in .text can happen to form a valid instruction encoding. LLVM puts tables in .rodata so ELF builds are usually clean; MSVC inlines jump tables and static const arrays into .text.

Signs of a false positive:

  • Symbol is a lookup table or a function you know contains no SIMD.
  • Reported instruction count is tiny (13) inside an otherwise-non-SIMD symbol.
  • objdump -d around the reported address shows ret then byte soup — no stack frame setup, no control flow leading to it.

If confirmed: allowlist the symbol as a blanket pass (bare name, no [...] bracket). The reported features are misdecoded data bytes whose values move with link layout, not gated code, so a ceiling has nothing to bound and just re-flakes on the next layout that decodes differently. Note the reason in the group comment.

Adding an allowlist entry

Append the symbol to the appropriate file. Group with its neighbors under the existing # Gate: ... header; if no existing group matches, add one:

# ----------------------------------------------------------------------------
# <dependency> <variant>. Gate: <what checks CPUID/HWCAP>.
# (N symbols)
# ----------------------------------------------------------------------------
symbol_name_exactly_as_the_tool_printed_it  [FEAT1, FEAT2]

Use a feature ceiling ([...]) for gated code. A blanket pass (no brackets) defeats the "did the gate get updated when the dep grew AVX-512?" check (src/main.rs:616-621). List exactly the features the tool reported; that's what the gate currently checks. The exceptions are confirmed data-in-.text misdecodes (previous section) and <no-symbol@...> padding (below): there is no gate to drift past, so blanket-pass those.

x64 feature names (iced-x86 Debug strings — must match exactly): AVX, AVX2, FMA, FMA4, BMI1, BMI2, MOVBE, ADX, RDRAND, AES, PCLMULQDQ, VAES, VPCLMULQDQ, SHA, AVX512F, AVX512BW, AVX512DQ, AVX512VL, AVX512_VBMI, AVX512_VBMI2, AVX512_VNNI, AVX512_VPOPCNTDQ, AVX512_FP16, AVX_VNNI, …

aarch64 feature names: LSE, SVE, RCPC, DotProd, JSCVT, RDM, PAC(non-hint).

Special symbol forms

Rust v0 mangling — <rust-hash>. Rust symbols contain a crate-hash (Cs[base62]_) that changes across target triples and toolchains. The tool canonicalizes both sides (src/main.rs:196-227), so allowlist entries should use <rust-hash> in place of the hash:

# Tool reports:
  _RNvMNtNtNtNtCs5QMN7YRSXc3_6memchr4arch6x86_644avx26memchrNtB2_3One13find_raw_avx2  [AVX, AVX2]
# Allowlist as:
  _RNvMNtNtNtNt<rust-hash>6memchr4arch6x86_644avx26memchrNtB2_3One13find_raw_avx2  [AVX, AVX2]

Either form works (the tool canonicalizes both before comparing), but <rust-hash> survives toolchain bumps.

Windows <lib:NAME.lib>. When PDB has no per-function record for a hit (stripped CRT objects, anonymized staticlib helpers), the tool falls back to section-contribution attribution: the linker-map "which .lib did this byte come from" data. These attributions are stable across link layout changes. Allowlist them literally:

<lib:lolhtml.lib>  [AVX, AVX2]

<no-symbol@0x...> — the address fell in padding between functions or the binary is stripped. If you see these for every violation, you're scanning the wrong binary (use -profile). If it's just one or two, it's usually inter- function padding that decoded as something; investigate with objdump -d around that address and, if it's genuinely junk, add a brief # padding at <addr range> comment with a blanket-pass entry.

PDB coverage drift (Windows)

A function may get an S_LPROC32 record (real mangled name) on one toolchain and fall through to <lib:...> on another. If the same code flips between forms across CI runs, allowlist both.

Deliberately ignored (not reported even if found)

See src/main.rs:94-135 and src/aarch64.rs:

  • TZCNT (x64) — decodes as REP BSF on pre-BMI1; LLVM preloads dest with operand-width so the src==0 case matches. (LZCNT is NOT ignored — BSRLZCNT for nonzero inputs and LLVM never emits it for Nehalem.)
  • XGETBV (x64) — needed by every AVX gate; a stray one SIGILLs at startup so the emulator catches it trivially.
  • ENDBR64 (CET_IBT), RDSSP/INCSSP (CET_SS hint-space subset) (x64) — NOP-encoded on pre-CET by design. The rest of CET_SS (WRSSD/RSTORSSP/ SETSSBSY etc.) IS flagged — dedicated opcode slots that #UD on pre-CET.
  • PACIASP/AUTIASP/BTI (aarch64) — HINT-space, architecturally NOP on pre-PAC CPUs. (LDRAA/LDRAB are not HINT-space and are reported.)
  • 3DNow!, SMM, Cyrix, VIA, RTM/TSX (x64) — no toolchain targeting x86-64 emits these without explicit intrinsics. When their encodings show up (0f 0f 3DNow!, C7/C6 F8 XBEGIN/XABORT), it's data.