diff --git a/scripts/verify-baseline-static/CLAUDE.md b/scripts/verify-baseline-static/CLAUDE.md index c8b7633bba9e..7d1a60018570 100644 --- a/scripts/verify-baseline-static/CLAUDE.md +++ b/scripts/verify-baseline-static/CLAUDE.md @@ -31,8 +31,9 @@ check — together they catch most things; neither alone is bulletproof. **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 + that follow, up to the next symbol start (where the decoder resyncs). + Variable-length x86 encoding makes perfect code/data separation + undecidable (`README.md:53-62`). 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 @@ -43,7 +44,7 @@ check — together they catch most things; neither alone is bulletproof. - 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`. + inlines jump tables). See `README.md:64-77`. 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 diff --git a/scripts/verify-baseline-static/README.md b/scripts/verify-baseline-static/README.md index 3417ec52ec9a..951ca2857be7 100644 --- a/scripts/verify-baseline-static/README.md +++ b/scripts/verify-baseline-static/README.md @@ -52,10 +52,13 @@ If you're not sure which: run the binary under `qemu-x86_64 -cpu Nehalem` ### 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 +The tool linear-sweeps every byte in `.text`, restarting the decoder at every +symbol start (an instruction that would straddle one is discarded), so data at +the end of a function — JSC's LLInt puts a 4-byte opcode id in front of every +`llint_op_*` label — cannot desynchronise the decode of the functions after +it. Within one symbol 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](https://www2.cs.arizona.edu/~debray/Publications/disasm.pdf)). MSVC inlines jump tables and small `static const` arrays into `.text` right diff --git a/scripts/verify-baseline-static/allowlist-x64.txt b/scripts/verify-baseline-static/allowlist-x64.txt index 2b2a7b5c4ea9..42ce39c5d0c7 100644 --- a/scripts/verify-baseline-static/allowlist-x64.txt +++ b/scripts/verify-baseline-static/allowlist-x64.txt @@ -1352,24 +1352,3 @@ jsimd_ycc_rgb_convert_avx2.out1 [AVX, AVX2] jsimd_ycc_rgb_convert_avx2.return [AVX, AVX2] -# ---------------------------------------------------------------------------- -# JSC LLInt. Data-in-.text false positive, NOT a gate. -# -# ENABLE(LLINT_EMBEDDED_OPCODE_ID) is on for x86_64 (WTF/wtf/PlatformEnable.h), -# so every LLInt opcode handler starts with a raw 4-byte ".int " -# (LowLevelInterpreter.cpp: EMBED_OPCODE_ID_IF_NEEDED). The linear-sweep -# decoder desyncs on those embedded ints and, depending on how preceding .text -# code aligns, can decode a stray VEX-encoded instruction (or an RDPMC, 0F 33) -# out of the garbage. offlineasm's x86 backend emits SSE2 (not VEX), and the -# Intel SDE Nehalem emulation step in this same job exercises the LLInt and -# passes, so any single-digit hit here is decode noise. Blanket pass, as for -# any data-in-.text misdecode: the handlers are offlineasm output, not compiler -# output, and the decoded feature moves with link layout (the same desync has -# decoded as AVX and as RDPMC), so a ceiling has nothing to bound. On ELF each -# opcode handler has its own symbol, so layout shifts can move the desync -# between llint_op_* siblings; add them here as they trip. -# (3 symbols) -# ---------------------------------------------------------------------------- -llint_op_enter_wide32 -llint_op_jmp_wide32 -llint_op_wide16_wide16 diff --git a/scripts/verify-baseline-static/src/main.rs b/scripts/verify-baseline-static/src/main.rs index c713102fa496..9bd671184347 100644 --- a/scripts/verify-baseline-static/src/main.rs +++ b/scripts/verify-baseline-static/src/main.rs @@ -775,14 +775,40 @@ fn scan_x86_64(bytes: &[u8], sec_addr: u64, syms: &[Sym], allowlist: &Allowlist) let mut allowlisted = Buckets::new(); let mut total_insns = 0u64; - // Linear sweep. iced handles variable-length encoding; on undecodable - // bytes (data-in-text) it returns Code::INVALID and we skip. False - // positives from data-in-text are rare in practice — LLVM puts jump - // tables in .rodata, not inline. + // Linear sweep, resynchronised at every symbol start. iced handles + // variable-length encoding; on undecodable bytes it returns Code::INVALID + // and we skip. Data in .text (JSC's LLInt puts a 4-byte opcode id in front + // of every llint_op_* label; hand-written asm occasionally has constants + // after a ret) desyncs a pure linear sweep, and what the following bytes + // then decode as depends on link layout. Every symbol start is a real + // instruction boundary, so an instruction that would straddle one is + // garbage: drop it and restart decoding at the symbol. + let sec_end = sec_addr + bytes.len() as u64; + let mut starts: Vec = syms + .iter() + .map(|s| s.addr) + .filter(|&a| a > sec_addr && a < sec_end) + .collect(); + starts.sort_unstable(); + starts.dedup(); + let mut next_start = 0usize; + let mut decoder = Decoder::with_ip(64, bytes, sec_addr, DecoderOptions::NONE); let mut insn = Instruction::default(); while decoder.can_decode() { decoder.decode_out(&mut insn); + let (ip, next_ip) = (insn.ip(), decoder.ip()); + while next_start < starts.len() && starts[next_start] <= ip { + next_start += 1; + } + if next_start < starts.len() && starts[next_start] < next_ip { + let resync = starts[next_start]; + decoder + .set_position((resync - sec_addr) as usize) + .expect("symbol start inside section"); + decoder.set_ip(resync); + continue; + } if insn.is_invalid() { continue; }