Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions scripts/verify-baseline-static/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 7 additions & 4 deletions scripts/verify-baseline-static/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 0 additions & 21 deletions scripts/verify-baseline-static/allowlist-x64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <opcode_id>"
# (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
34 changes: 30 additions & 4 deletions scripts/verify-baseline-static/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> = 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;
}
Expand Down