Merge merge/rainworld-gate d4da17b8: tier-1 slot-target check before the page check, JIT-code guard captures bytes before decode (smc-race), quadratic tier-1 emission fix, gex-gl shader-name and glTextureView fixes, off-by-default diagnostics

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_461fe3ff06374ecba693831148697ba6
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-06 17:55:05 -04:00
commit 132f73ed63
10 files changed
+344 -29

No files matched your search

+73 -16
View File
@@ -340,6 +340,19 @@ fn trace_blocks() -> bool {
|| *ON.get_or_init(|| std::env::var_os("GEX_TRACE_BLOCKS").is_some())
}
/// `GEX_TRACE_RANGE=LO-HI` (hex): trace every block starting in the range
/// (its translation, and each execution through `BRK_TRACE`).
fn in_trace_range(pc: u64) -> bool {
static R: std::sync::OnceLock<Option<(u64, u64)>> = std::sync::OnceLock::new();
let r = R.get_or_init(|| {
let v = std::env::var("GEX_TRACE_RANGE").ok()?;
let (a, b) = v.split_once('-')?;
let h = |x: &str| u64::from_str_radix(x.trim().trim_start_matches("0x"), 16).ok();
Some((h(a)?, h(b)?))
});
r.is_some_and(|(a, b)| pc >= a && pc < b)
}
fn trap_pc() -> u64 {
static ENV: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
let e = *ENV.get_or_init(|| {
@@ -368,6 +381,15 @@ fn trap_pc() -> u64 {
/// a patch cannot change a body's length.
pub fn compile_with_relocs(block: &Block, cx: &Context<'_>) -> (Vec<u8>, Vec<u32>) {
let mut e = Emitter::new(cx, block.start);
// First, before the page check: a block entered for a neighbour in its
// dispatch slot (x86-64's 4-byte slots) must hand over to that address.
// The page check's stale path (and the guard's) dispatch to this block's
// own start, which for a neighbour's branch is the wrong address: a call
// to a JIT trampoline at E ran a stale block for E+2 there, mid-
// instruction (Rain World's crashes in Mono's trampolines).
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
e.emit_target_check(block.start);
}
if let Some(bases) = cx.page_bases.filter(|_| page_check()) {
let mut pages = checked_pages(bases, block.start, block.end);
// A region carries its members: their pages too.
@@ -392,11 +414,18 @@ pub fn compile_with_relocs(block: &Block, cx: &Context<'_>) -> (Vec<u8>, Vec<u32
if is_trap_pc(block.start) {
e.emit_trap_pc(block.start);
}
if trace_blocks() {
if trace_blocks() || in_trace_range(block.start) {
e.emit_trace_point(block.start);
}
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
e.emit_target_check(block.start);
if in_trace_range(block.start) {
eprintln!(
"[t1] translate {:#x}..{:#x} guard {} term {:?} bytes {}",
block.start,
block.end,
block.guard.as_ref().map_or(0, |g| g.len()),
block.term,
block.guard.as_ref().map_or(String::new(), |g| g.iter().take(64).map(|b| format!("{b:02x}")).collect::<String>())
);
}
if let Some(g) = &block.guard {
e.emit_guard(block.start, g);
@@ -407,6 +436,15 @@ pub fn compile_with_relocs(block: &Block, cx: &Context<'_>) -> (Vec<u8>, Vec<u32
pub fn compile(block: &Block, cx: &Context<'_>) -> Vec<u8> {
let mut e = Emitter::new(cx, block.start);
// First, before the page check: a block entered for a neighbour in its
// dispatch slot (x86-64's 4-byte slots) must hand over to that address.
// The page check's stale path (and the guard's) dispatch to this block's
// own start, which for a neighbour's branch is the wrong address: a call
// to a JIT trampoline at E ran a stale block for E+2 there, mid-
// instruction (Rain World's crashes in Mono's trampolines).
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
e.emit_target_check(block.start);
}
if let Some(bases) = cx.page_bases.filter(|_| page_check()) {
let mut pages = checked_pages(bases, block.start, block.end);
// A region carries its members: their pages too.
@@ -431,11 +469,18 @@ pub fn compile(block: &Block, cx: &Context<'_>) -> Vec<u8> {
if is_trap_pc(block.start) {
e.emit_trap_pc(block.start);
}
if trace_blocks() {
if trace_blocks() || in_trace_range(block.start) {
e.emit_trace_point(block.start);
}
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
e.emit_target_check(block.start);
if in_trace_range(block.start) {
eprintln!(
"[t1] translate {:#x}..{:#x} guard {} term {:?} bytes {}",
block.start,
block.end,
block.guard.as_ref().map_or(0, |g| g.len()),
block.term,
block.guard.as_ref().map_or(String::new(), |g| g.iter().take(64).map(|b| format!("{b:02x}")).collect::<String>())
);
}
if let Some(g) = &block.guard {
e.emit_guard(block.start, g);
@@ -790,14 +835,12 @@ impl<'a> Emitter<'a> {
self.value_local = vec![None; n];
self.value_const = vec![None; n];
self.value_ty = vec![Ty::I64; n];
let last_use = compute_last_use(block);
let dying = dying_after(block);
self.prof_entry(block);
for (i, stmt) in block.stmts.iter().enumerate() {
self.stmt(stmt, block);
for (v, &lu) in last_use.iter().enumerate() {
if lu == Some(i) {
self.release(Value(v as u32));
}
for &v in &dying[i] {
self.release(Value(v));
}
}
self.region_terminator(block);
@@ -866,7 +909,7 @@ impl<'a> Emitter<'a> {
self.value_const = vec![None; n];
self.value_ty = vec![Ty::I64; n];
let last_use = compute_last_use(block);
let dying = dying_after(block);
// A block whose only successor is itself becomes a real wasm loop, so
// its guest registers stay in locals across the back-edge. Sound only
@@ -890,10 +933,8 @@ impl<'a> Emitter<'a> {
for (i, stmt) in block.stmts.iter().enumerate() {
self.stmt(stmt, block);
// Recycle locals whose values are dead after this statement.
for (v, &lu) in last_use.iter().enumerate() {
if lu == Some(i) {
self.release(Value(v as u32));
}
for &v in &dying[i] {
self.release(Value(v));
}
}
self.terminator(block);
@@ -4927,6 +4968,22 @@ impl<'a> Emitter<'a> {
}
}
/// For each statement, the values whose last use it is, in value order (the
/// order the releases always ran in). Was a scan of every value after every
/// statement: quadratic, and most of a Unity start-up's time in Mono's large
/// JIT-generated blocks.
fn dying_after(block: &Block) -> Vec<Vec<u32>> {
let mut out = vec![Vec::new(); block.stmts.len()];
for (v, lu) in compute_last_use(block).into_iter().enumerate() {
if let Some(i) = lu {
if let Some(d) = out.get_mut(i) {
d.push(v as u32);
}
}
}
out
}
/// Index of the last statement that reads each value, or `None` when the
/// value is read by the terminator (and so must survive the whole block).
fn compute_last_use(block: &Block) -> Vec<Option<usize>> {
+40 -2
View File
@@ -170,6 +170,7 @@ mod ffi {
);
pub fn glGenTextures(n: GLsizei, textures: *mut GLuint);
pub fn glIsTexture(texture: GLuint) -> GLboolean;
pub fn glDeleteTextures(n: GLsizei, textures: *const GLuint);
pub fn glBindTexture(target: GLenum, texture: GLuint);
pub fn glActiveTexture(unit: GLenum);
@@ -1102,9 +1103,14 @@ impl Gl {
}
}
op::DELETE_SHADER => {
// GL keeps a deleted shader that is still attached to a
// program, and its name stays usable until it is
// detached: wined3d attaches, deletes, and only then
// compiles (its format checks' "draw quad"). So the name
// keeps its mapping; a new shader given the same name
// replaces it.
if let Some(n) = a.u32() {
if let Some(real) = self.names.shaders.remove(&n) {
native::gexgl_native_name(0, n, 0);
if let Some(&real) = self.names.shaders.get(&n) {
glDeleteShader(real);
}
}
@@ -1643,6 +1649,11 @@ impl Gl {
// compatibility profile) are decoded by the C++ shared with
// the browser's host.
_ => {
// glTextureView (gl44 1422, compat 10871): its new name
// must be one the host generated and never bound.
if (opcode == 1422 || opcode == 10871) && body.len() >= 4 {
reserve_texture_name(u32::from_le_bytes([body[0], body[1], body[2], body[3]]));
}
let (mut out, mut len) = (std::ptr::null(), 0usize);
if native::gexgl_native_one(opcode, body.as_ptr(), body.len(), &mut out, &mut len) != 1 {
return false;
@@ -1657,6 +1668,33 @@ impl Gl {
}
}
/// Make `name` a texture name the host's GL generated and has not bound, as
/// glTextureView requires of its new texture (the guest picks texture names
/// and GL otherwise meets them first at bind time, so a view's name was never
/// valid and wined3d's views came out empty: Rain World drew nothing).
/// Generated names are the lowest free ones, so generate until it comes up and
/// give back the rest.
unsafe fn reserve_texture_name(name: GLuint) {
if name == 0 || glIsTexture(name) != 0 {
return;
}
let mut spare = Vec::new();
for _ in 0..65536 {
let mut n: GLuint = 0;
glGenTextures(1, &mut n);
if n == name || n == 0 || n > name {
if n != name && n != 0 {
spare.push(n);
}
break;
}
spare.push(n);
}
if !spare.is_empty() {
glDeleteTextures(spare.len() as GLsizei, spare.as_ptr());
}
}
/// native/gexgl-native.cpp (built by build.rs).
mod native {
extern "C" {
+2 -2
View File
@@ -885,10 +885,10 @@ pub fn hardware_exception(
return Outcome::Exit(code);
};
let abi = p.abi;
if p.cfg.trace {
if p.cfg.trace || std::env::var_os("GEX_NT_TRACE_EXC").is_some() {
let r = |o: usize| g.u64(ctx_addr + o as u64).unwrap_or(0);
crate::nt_log!(
"[nt:{:x}] exception {code:#010x} at {} (sp {:#x}, last indirect branch {:#x} from block {:#x})",
"[nt:{:x}] exception {code:#010x} {info:x?} at {} (sp {:#x}, last indirect branch {:#x} from block {:#x})",
t.tid,
p.describe_addr(rip),
r(ctx::x(4)),
+22 -5
View File
@@ -1409,6 +1409,15 @@ fn lift_one(
executable: &(dyn Fn(u64) -> bool + Sync),
limits: BlockLimits,
) -> (Block, Option<u64>) {
// The guard's bytes are taken before the block is decoded: another
// thread may be writing this code (a JIT emitting a method or a
// trampoline next to code already running), and bytes read after the
// decode would vouch for a translation of the earlier ones.
let snapshot: Option<Vec<u8>> = is_volatile(addr).then(|| {
let lo = (addr as usize).min(memory.len());
let hi = lo.saturating_add(GUARD_MAX).min(memory.len());
memory[lo..hi].to_vec()
});
let fetch = |pc: u64, buf: &mut arch::FetchBuf| fetch_code_bytes(memory, executable, pc, buf);
let mut block = lift_block(fetch, addr, limits);
let read = |a: u64, bytes: u32| -> Option<u64> {
@@ -1437,11 +1446,19 @@ fn lift_one(
}
pass::optimize(&mut block);
// Self-modifying code with no flush instruction to announce it.
if is_volatile(addr) {
let lo = addr as usize;
let hi = (block.end as usize).max(lo + 1).min(lo + GUARD_MAX).min(memory.len());
if hi > lo {
block.guard = Some(memory[lo..hi].to_vec());
// TEST(GEX_TEST_LIFT_DELAY_US): widen the window between decoding a
// JIT-code block and fixing its guard, for tests/x86-programs/smc-race.
if snapshot.is_some() {
static DELAY: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
let d = *DELAY.get_or_init(|| std::env::var("GEX_TEST_LIFT_DELAY_US").ok().and_then(|v| v.parse().ok()).unwrap_or(0));
if d > 0 {
std::thread::sleep(std::time::Duration::from_micros(d));
}
}
if let Some(snap) = snapshot {
let n = ((block.end as usize).max(addr as usize + 1) - addr as usize).min(snap.len());
if n > 0 {
block.guard = Some(snap[..n].to_vec());
}
}
// x86-64 and PowerPC: folded on a scratch copy only, never in the
+84 -3
View File
@@ -3362,6 +3362,7 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
let gm = GuestMem::new(m.bytes());
let bytes: Vec<String> = gm.read(pc as u64, 16).map(|b| b.iter().map(|x| format!("{x:02x}")).collect()).unwrap_or_default();
eprintln!("gex-nt: illegal instruction at {:#x}: {}", pc as u64, bytes.join(" "));
fault_context(&gm, _ctx, pc as u64);
}
return nt::fault_at(&mut caller, nt::Fault::Illegal, pc as u64, resumable);
}
@@ -3423,11 +3424,26 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
let data = memory.bytes();
let c = ctx as u32 as usize;
let r = |i: u8| read_u64(data, c + ctx::x(i));
if trace_ring_on() {
let sp = r(4) as usize;
let top = data.get(sp..sp + 8).map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap()));
let e = [pc as u64, sp as u64, top, read_u64(data, c + ctx::LAST_TARGET), read_u64(data, c + ctx::LAST_SOURCE)];
TRACE_RING.with(|t| {
let mut t = t.borrow_mut();
if t.len() >= 512 {
t.pop_front();
}
t.push_back(e);
});
return Ok(());
}
if gex_abi::X86 {
eprintln!(
"[trace] pc={pc:#x} rax={:#x} rcx={:#x} rdx={:#x} rbx={:#x} rsp={:#x} rbp={:#x} rsi={:#x} rdi={:#x} r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x} fl={:#x}",
r(0), r(1), r(2), r(3), r(4), r(5), r(6), r(7), r(8), r(9), r(10), r(11), r(12), r(13), r(14), r(15),
read_u64(data, c + ctx::NZCV)
"[trace] pc={pc:#x} tid={} rax={:#x} rcx={:#x} rdx={:#x} rbx={:#x} rsp={:#x} rbp={:#x} rsi={:#x} rdi={:#x} r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x} fl={:#x} [rsp]={:#x} [rsp+8]={:#x} last={:#x}<-{:#x}",
ctx, r(0), r(1), r(2), r(3), r(4), r(5), r(6), r(7), r(8), r(9), r(10), r(11), r(12), r(13), r(14), r(15),
read_u64(data, c + ctx::NZCV),
data.get(r(4) as usize..r(4) as usize + 8).map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap())), data.get(r(4) as usize + 8..r(4) as usize + 16).map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap())),
read_u64(data, c + ctx::LAST_TARGET), read_u64(data, c + ctx::LAST_SOURCE)
);
} else {
let regs: Vec<String> = (0..31u8).map(|i| format!("x{i}={:#x}", r(i))).collect();
@@ -3441,6 +3457,32 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
return Ok(());
}
if caller.data().nt.is_some() {
// A privileged instruction: log the bytes there now and the
// block that trapped, so a stale or misaligned translation
// (bytes that are no longer `hlt`/`in`/`out`/`int n`) shows.
if trace_ring_on() && imm != gex_abi::imports::BRK_GENERAL_PROTECTION {
if let Some(m) = caller.data().memory.clone() {
eprintln!("gex-nt: fault {imm} at {:#x}", pc as u64);
fault_context(&GuestMem::new(m.bytes()), ctx, pc as u64);
}
}
if imm == gex_abi::imports::BRK_GENERAL_PROTECTION {
if let Some(m) = caller.data().memory.clone() {
let gm = GuestMem::new(m.bytes());
let at = |a: u64| -> String { gm.read(a, 16).map(|b| b.iter().map(|x| format!("{x:02x}")).collect::<Vec<_>>().join(" ")).unwrap_or_default() };
let c = ctx as u32 as u64;
let src = gm.read_u64(c + ctx::LAST_SOURCE as u64).unwrap_or(0);
let tgt = gm.read_u64(c + ctx::LAST_TARGET as u64).unwrap_or(0);
eprintln!(
"gex-nt: privileged instruction at {:#x}: {} (last source {src:#x}: {}; last target {tgt:#x}: {})",
pc as u64,
at(pc as u64),
at(src),
at(tgt)
);
fault_context(&gm, ctx, pc as u64);
}
}
return nt::fault_at(&mut caller, nt::Fault::from_brk(imm), pc as u64, resumable);
}
// A divide error (`Stmt::Trap`): x86 #DE. Linux reports a zero
@@ -3731,6 +3773,9 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
}
if op == gex_abi::cache_op::IC_INVALIDATE {
let addr = addr as u64;
if std::env::var_os("GEX_TRACE_GUARD").is_some() {
eprintln!("[guard] {addr:#x} changed (block {:#x})", pc as u64);
}
let page = addr / gex_abi::DISPATCH_PAGE;
retire_translations(
&mut caller,
@@ -5095,3 +5140,39 @@ pub(crate) fn check_i386_high(cx: &impl wasmtime::AsContext<Data = HostState>, s
}
}
}
/// The bytes around a faulting pc and the top of the guest stack (NT
/// faults; RW debugging of Mono JIT code).
fn trace_ring_on() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var_os("GEX_TRACE_RING").is_some())
}
thread_local! {
/// `GEX_TRACE_RING=1`: the last blocks this thread entered among those
/// traced (`GEX_TRACE_RANGE`): pc, rsp, [rsp], last target, last source.
static TRACE_RING: std::cell::RefCell<std::collections::VecDeque<[u64; 5]>> = Default::default();
}
fn fault_context(gm: &GuestMem<'_>, ctx: i32, pc: u64) {
TRACE_RING.with(|t| {
for e in t.borrow().iter() {
eprintln!("gex-nt: ring pc {:#x} rsp {:#x} [rsp] {:#x} last {:#x}<-{:#x}", e[0], e[1], e[2], e[3], e[4]);
}
});
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<Vec<_>>().join(" ");
if let Ok(b) = gm.read(pc.saturating_sub(64), 96) {
eprintln!("gex-nt: bytes {:#x}: {}", pc.saturating_sub(64), hex(&b));
}
let c = ctx as u32 as u64;
let sp = gm.read_u64(c + gex_abi::ctx::X as u64 + 4 * 8).unwrap_or(0);
let words: Vec<String> = (0..12).map(|i| format!("{:#x}", gm.read_u64(sp + 8 * i).unwrap_or(0))).collect();
eprintln!("gex-nt: rsp {sp:#x}: {}", words.join(" "));
let ret = gm.read_u64(sp).unwrap_or(0);
if let Ok(b) = gm.read(ret.saturating_sub(32), 40) {
eprintln!("gex-nt: before [rsp] {:#x}: {}", ret.saturating_sub(32), hex(&b));
}
let names = ["rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"];
let regs: Vec<String> = (0..16).map(|i| format!("{}={:#x}", names[i], gm.read_u64(c + gex_abi::ctx::X as u64 + 8 * i as u64).unwrap_or(0))).collect();
eprintln!("gex-nt: regs {}", regs.join(" "));
}
Binary file not shown.
Binary file not shown.
+34
View File
@@ -44,6 +44,9 @@ PROC(void, glGetProgramiv, (GLuint, GLenum, GLint *))
PROC(void, glUniform4f, (GLint, float, float, float, float))
PROC(GLint, glGetUniformLocation, (GLuint, const char *))
PROC(void, glGetUniformfv, (GLuint, GLint, float *))
PROC(void, glDeleteShader, (GLuint))
PROC(void, glTexStorage2D, (GLenum, GLsizei, GLenum, GLsizei, GLsizei))
PROC(void, glTextureView, (GLuint, GLenum, GLuint, GLenum, GLuint, GLuint, GLuint, GLuint))
#define LOAD(name) p##name = (name##_t)wglGetProcAddress(#name)
static LRESULT CALLBACK proc(HWND w, UINT m, WPARAM wp, LPARAM lp) { return DefWindowProcW(w, m, wp, lp); }
@@ -95,6 +98,7 @@ int main(void) {
LOAD(glEnableVertexAttribArray); LOAD(glCreateShader); LOAD(glShaderSource); LOAD(glCompileShader);
LOAD(glCreateProgram); LOAD(glAttachShader); LOAD(glBindAttribLocation); LOAD(glLinkProgram);
LOAD(glUseProgram); LOAD(glGetProgramiv); LOAD(glUniform4f); LOAD(glGetUniformLocation); LOAD(glGetUniformfv);
LOAD(glDeleteShader); LOAD(glTexStorage2D); LOAD(glTextureView);
CHECK("GL 2.0 entry points", pglMapBuffer && pglVertexAttribPointer && pglGenQueries && pglCreateProgram);
if (!pglMapBuffer || !pglVertexAttribPointer || !pglGenQueries || !pglCreateProgram) goto out;
@@ -166,6 +170,36 @@ int main(void) {
CHECK("a client-array, client-index draw", px[0] < 16 && px[1] > 240);
pglUseProgram(0);
/* A shader deleted while attached, compiled only afterwards (wined3d's
* order): GL keeps it until it is detached, so the program still links. */
GLuint v2 = pglCreateShader(0x8B31), f2 = pglCreateShader(0x8B30), prog2 = pglCreateProgram();
pglShaderSource(v2, 1, &vs, 0);
pglShaderSource(f2, 1, &fs, 0);
pglAttachShader(prog2, v2);
pglAttachShader(prog2, f2);
pglDeleteShader(v2);
pglDeleteShader(f2);
pglCompileShader(v2);
pglCompileShader(f2);
pglLinkProgram(prog2);
GLint linked2 = 0;
pglGetProgramiv(prog2, 0x8B82 /* GL_LINK_STATUS */, &linked2);
CHECK("a program links from shaders deleted while attached", linked2);
/* A texture view: its name has to be one GL generated and never bound
* (wined3d's D3D11 views). The guest names textures itself. */
if (pglTexStorage2D && pglTextureView) {
while (glGetError() != GL_NO_ERROR) {
}
GLuint t[2];
glGenTextures(2, t);
glBindTexture(GL_TEXTURE_2D, t[0]);
pglTexStorage2D(GL_TEXTURE_2D, 1, 0x8058 /* GL_RGBA8 */, 4, 4);
pglTextureView(t[1], GL_TEXTURE_2D, t[0], 0x8058, 0, 1, 0, 1);
CHECK("glTextureView on a generated name", glGetError() == GL_NO_ERROR);
glBindTexture(GL_TEXTURE_2D, 0);
}
out:
CHECK("no GL error", glGetError() == GL_NO_ERROR);
CHECK("SwapBuffers returns TRUE", SwapBuffers(dc) == TRUE);
+5 -1
View File
@@ -31,7 +31,11 @@ for src in "$HERE"/*.c; do
bin="$WORK/$name-$target"
[ -x "$bin" ] || zig cc -target "$target" -O1 -static $extra -o "$bin" "$src" || { fail=1; continue; }
native=$("$bin" 2>/dev/null); ns=$?
gex=$("$GEXC" run $flags "$bin" 2>/dev/null); gs=$?
# smc-race: the translator waits between decoding a JIT-code block
# and fixing its guard, so the writer's stores land in between.
envs=""
[ $name = smc-race ] && envs="GEX_TEST_LIFT_DELAY_US=300"
gex=$(env $envs "$GEXC" run $flags "$bin" 2>/dev/null); gs=$?
if [ "$native" = "$gex" ] && [ $ns = $gs ]; then
echo "ok $name ($target, $tier, exit $ns)"
else
+84
View File
@@ -0,0 +1,84 @@
/* A JIT writing code while another thread translates and runs it: the writer
* stores a new function (`mov eax, N; ret`, N new each round) over the old
* one with a single aligned 8-byte store, while the caller keeps calling it,
* so translations of the function race with the writes. After each round
* both threads meet, and the call must return that round's N: a block
* guard that recorded the bytes after decoding (newer than what the block
* was translated from) would let a stale translation answer for ever. */
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
/* Spin for about `us` microseconds: writes spaced out like this land while
* the other thread is translating the function they rewrite. */
static void spin(long us) {
struct timespec a, b;
clock_gettime(CLOCK_MONOTONIC, &a);
do clock_gettime(CLOCK_MONOTONIC, &b);
while ((b.tv_sec - a.tv_sec) * 1000000L + (b.tv_nsec - a.tv_nsec) / 1000 < us);
}
#ifndef ROUNDS
#define ROUNDS 200
#endif
#ifndef CHURN
#define CHURN 20
#endif
typedef uint64_t __attribute__((aligned(8))) u64a; /* i386: 8-byte atomic stores */
static volatile u64a *fn;
static atomic_int round_no, done_writing;
static uint64_t code_for(int n) {
unsigned char c[8] = {0xb8, 0, 0, 0, 0, 0xc3, 0xcc, 0xcc}; /* mov eax, n; ret */
memcpy(c + 1, &n, 4);
uint64_t w;
memcpy(&w, c, 8);
return w;
}
static void *writer(void *arg) {
(void)arg;
for (int r = 1; r <= ROUNDS; r++) {
/* Churn: many versions while the caller is calling. */
for (int k = 0; k < CHURN; k++) {
__atomic_store_n(fn, code_for(r * 1000 + k), __ATOMIC_SEQ_CST);
spin(100 + 37 * (k % 7));
}
__atomic_store_n(fn, code_for(r * 1000 + 999), __ATOMIC_SEQ_CST);
spin(2000);
atomic_store(&done_writing, r);
while (atomic_load(&round_no) != r) {
}
}
return 0;
}
int main(void) {
unsigned char *page = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED) return 2;
fn = (volatile u64a *)(page + 0x100);
*fn = code_for(0);
pthread_t w;
pthread_create(&w, 0, writer, 0);
long bad = 0, calls = 0;
for (int r = 1; r <= ROUNDS; r++) {
while (atomic_load(&done_writing) != r) {
((int (*)(void))(uintptr_t)fn)();
calls++;
}
for (int k = 0; k < 3; k++) {
int got = ((int (*)(void))(uintptr_t)fn)();
if (got != r * 1000 + 999) bad++;
}
atomic_store(&round_no, r);
}
pthread_join(w, 0);
(void)calls;
printf("%d rounds, %ld stale results\n", ROUNDS, bad);
return bad != 0;
}