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

verify-baseline-static

Static ISA verifier for Bun baseline builds. Disassembles every instruction in .text and flags anything requiring CPU features beyond the baseline target.

Target Baseline Flags
x86-64 Nehalem (-march=nehalem) AVX, AVX2, AVX-512, BMI1/2, FMA, AES-NI, PCLMULQDQ, ADX, SHA-NI, RDRAND, ...
aarch64 armv8-a+crc (-march=armv8-a+crc) LSE atomics, SVE, DotProd, RCPC, JSCVT, RDM, non-hint PAC

Architecture and format (ELF/PE) are auto-detected from the binary header.

Complements the emulator-based check in scripts/verify-baseline.ts: the emulator only catches instructions the test suite executes. This catches everything the compiler emitted.

Usage

cargo build --release --manifest-path scripts/verify-baseline-static/Cargo.toml

./scripts/verify-baseline-static/target/release/verify-baseline-static \
  --binary path/to/bun-linux-x64-baseline/bun-profile \
  --allowlist scripts/verify-baseline-static/allowlist-x64.txt

--allowlist can be repeated; entries merge (feature ceilings union, blanket-pass wins). Useful for layering an extra allowlist on top of the base one without editing it.

Exit 0 = clean, 1 = violations, 2 = tool error.

Use the -profile artifact. The stripped release binary has no .symtab (ELF) and no companion .pdb (PE) — every violation becomes <no-symbol@addr> and nothing in the allowlist matches. Windows auto-discovers <binary>.pdb if present, or pass --pdb explicitly.

When it fails

A new symbol showed up with post-baseline instructions. Two possibilities:

  1. It's runtime-dispatched (CPUID / HWCAP-gated). Find the gate in the caller, add the symbol to the allowlist with a comment pointing at it. Group with related symbols.

  2. It's not gated. That's a real bug — a -march flag leaked into a subbuild and the function will SIGILL on baseline hardware. Fix the compile flags for that translation unit.

If you're not sure which: run the binary under qemu-x86_64 -cpu Nehalem (or qemu-aarch64 -cpu cortex-a53) and hit that code path. SIGILL = bug.

Data-in-.text false positives

The tool linear-sweeps every byte in .text. There's no general way to do better for x86: toolchains don't emit "this byte is data" markers the way ARM EABI's $d mapping symbols do, and code/data separation in x86 binaries is undecidable in general (Schwarz & Debray 2002).

MSVC inlines jump tables and small static const arrays into .text right after the function that uses them (LLVM puts them in .rodata, so ELF builds are typically clean). When those bytes form a valid instruction encoding, the decoder reports it.

Filtered automatically: 3DNow!, SMM, Cyrix, VIA Padlock, RTM/TSX — ISA extensions no toolchain targeting x86-64 emits without explicit intrinsics. Their encodings (0f xx for the defunct extensions, C7/C6 F8 for XBEGIN/XABORT) surface when a jump table's bytes happen to line up; the MSVC CRT strspn/strcspn/strpbrk switch tables are the observed source of the latter.

Not filtered (very rare): a table whose bytes form a valid VEX/EVEX-prefixed encoding. Looks like a real AVX hit. Triage the same way: disassemble around the address; if preceded by ret + small-int byte soup instead of stack-frame setup, it's a table. Allowlist the symbol.

What's deliberately ignored

x64:

  • ENDBR64 / CET_IBT — NOP-compatible on pre-CET CPUs by design.
  • TZCNT — LLVM preloads the destination with operand-width so the REP-BSF fallback on pre-BMI1 CPUs gives identical results. (LZCNT is not ignored: BSR and LZCNT produce different results, and LLVM never emits LZCNT for -march=nehalem.)
  • XGETBV — every AVX-dispatch path calls this, always after if (CPUID.OSXSAVE). A stray one would SIGILL at startup; the emulator test catches that trivially.

aarch64:

  • PACIASP / AUTIASP / BTI — HINT-space; architecturally NOP on older CPUs.
  • $d-marked data — literal pools and inline strings in .text are skipped via ARM EABI mapping symbols.

Symbol attribution

Binary Primary source Fallback
ELF .symtab .dynsym
PE PDB DBI module stream (S_*PROC32, has real sizes) → S_PUB32 PDB section contributions → <lib:NAME.lib>

The Windows fallback handles code with no per-function PDB record (stripped CRT objects, anonymized staticlib helpers). Section contributions are the linker-map data in structured form — they say which .obj/.lib every byte came from. Attribution is by library basename, which doesn't move when unrelated code shifts the link layout.

PDB coverage for the same code can vary across linker versions — a function that gets an S_LPROC32 record on one toolchain may fall through to <lib:> on another. If a symbol flips between a mangled name and <lib:NAME.lib> across CI runs, allowlist both forms.

Rust v0 mangled names carry a crate-hash (Cs[base62]_) that changes across target triples and toolchain versions. The allowlist uses <rust-hash> as a placeholder; the tool canonicalizes both sides before comparing.

Allowlist format

# comment
symbol_name                  # blanket pass — any feature allowed
symbol_name  [AVX, AVX2]     # feature ceiling — only these allowed

A feature outside the brackets is a violation even for an allowlisted symbol. Widening the bracket is the explicit checkpoint for "did the gate get updated when the dependency did?"

File Covers
allowlist-x64.txt linux-x64-baseline, linux-x64-musl-baseline
allowlist-x64-windows.txt windows-x64-baseline (separate: MSVC mangling, CRT symbols)
allowlist-aarch64.txt linux-aarch64, linux-aarch64-musl

One allowlist covers both glibc and musl because the dispatch surface is architecture-specific, not libc-specific. Symbols that LTO inlined away on one libc show as STALE on the other — informational, not an error.