# 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):

   ```sh
   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_DISPATCH` → `hwy::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):

```sh
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 (1–3) 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 —
  `BSR` ≠ `LZCNT` 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.
