Skip to content

ZJIT: A64 backend miscomp fixes (and a CI fix) #13904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 16, 2025
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ path = "jit.rs"

[features]
disasm = ["yjit?/disasm", "zjit?/disasm"]
runtime_checks = []
# TODO(GH-642) Turning this on trips a btest failure.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard for other contributors to figure out it refers to Shopify/ruby when you're looking at ruby/ruby, so it might be nice to mention the repository name. It confused me at first too.

runtime_checks = [] # ["yjit?/runtime_checks", "zjit?/runtime_checks"]
yjit = [ "dep:yjit" ]
zjit = [ "dep:zjit" ]

Expand Down
6 changes: 6 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -4014,10 +4014,16 @@ AS_IF([test x"$JIT_CARGO_SUPPORT" != "xno" -o \( x"$YJIT_SUPPORT" != "xno" -a x"
YJIT_LIBS=
ZJIT_LIBS=

# There's more processing below to get the feature set for the
# top-level crate, so capture at this point for feature set of
# just the zjit crate.
ZJIT_TEST_FEATURES="${rb_cargo_features}"

AS_IF([test x"${YJIT_SUPPORT}" != x"no"], [
rb_cargo_features="$rb_cargo_features,yjit"
])
AS_IF([test x"${ZJIT_SUPPORT}" != x"no"], [
AC_SUBST(ZJIT_TEST_FEATURES)
rb_cargo_features="$rb_cargo_features,zjit"
])
# if YJIT and ZJIT release mode
Expand Down
1 change: 1 addition & 0 deletions template/Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ ZJIT_OBJ=@ZJIT_OBJ@
JIT_CARGO_SUPPORT=@JIT_CARGO_SUPPORT@
CARGO_TARGET_DIR=@abs_top_builddir@/target
CARGO_BUILD_ARGS=@CARGO_BUILD_ARGS@
ZJIT_TEST_FEATURES=@ZJIT_TEST_FEATURES@
RUST_LIB=@RUST_LIB@
RUST_LIBOBJ = $(RUST_LIB:.a=.@OBJEXT@)
LDFLAGS = @STATIC@ $(CFLAGS) @LDFLAGS@
Expand Down
1 change: 1 addition & 0 deletions zjit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ expect-test = "1.5.1"
[features]
# Support --yjit-dump-disasm and RubyVM::YJIT.disasm using libcapstone.
disasm = ["capstone"]
runtime_checks = []
82 changes: 70 additions & 12 deletions zjit/src/backend/arm64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,26 +415,36 @@ impl Assembler
// being used. It is okay not to use their output here.
#[allow(unused_must_use)]
match &mut insn {
Insn::Sub { left, right, out } |
Insn::Add { left, right, out } => {
match (*left, *right) {
(Opnd::Reg(_) | Opnd::VReg { .. }, Opnd::Reg(_) | Opnd::VReg { .. }) => {
merge_three_reg_mov(&live_ranges, &mut iterator, left, right, out);
asm.push_insn(insn);
},
// When one operand is a register, legalize the other operand
// into possibly an immdiate and swap the order if necessary.
// Only the rhs of ADD can be an immediate, but addition is commutative.
(reg_opnd @ (Opnd::Reg(_) | Opnd::VReg { .. }), other_opnd) |
(other_opnd, reg_opnd @ (Opnd::Reg(_) | Opnd::VReg { .. })) => {
*left = reg_opnd;
*right = split_shifted_immediate(asm, other_opnd);
// Now `right` is either a register or an immediate, both can try to
// merge with a subsequent mov.
merge_three_reg_mov(&live_ranges, &mut iterator, left, left, out);
asm.push_insn(insn);
},
}
_ => {
*left = split_load_operand(asm, *left);
*right = split_shifted_immediate(asm, *right);
merge_three_reg_mov(&live_ranges, &mut iterator, left, right, out);
asm.push_insn(insn);
}
}
},
}
Insn::Sub { left, right, out } => {
*left = split_load_operand(asm, *left);
*right = split_shifted_immediate(asm, *right);
// Now `right` is either a register or an immediate,
// both can try to merge with a subsequent mov.
merge_three_reg_mov(&live_ranges, &mut iterator, left, left, out);
asm.push_insn(insn);
}
Insn::And { left, right, out } |
Insn::Or { left, right, out } |
Insn::Xor { left, right, out } => {
Expand Down Expand Up @@ -919,10 +929,28 @@ impl Assembler
ldp_post(cb, X29, X30, A64Opnd::new_mem(128, C_SP_REG, 16));
},
Insn::Add { left, right, out } => {
adds(cb, out.into(), left.into(), right.into());
// Usually, we issue ADDS, so you could branch on overflow, but ADDS with
// out=31 refers to out=XZR, which discards the sum. So, instead of ADDS
// (aliased to CMN in this case) we issue ADD instead which writes the sum
// to the stack pointer; we assume you got x31 from NATIVE_STACK_POINTER.
let out: A64Opnd = out.into();
if let A64Opnd::Reg(A64Reg { reg_no: 31, .. }) = out {
add(cb, out, left.into(), right.into());
} else {
adds(cb, out, left.into(), right.into());
}
},
Insn::Sub { left, right, out } => {
subs(cb, out.into(), left.into(), right.into());
// Usually, we issue SUBS, so you could branch on overflow, but SUBS with
// out=31 refers to out=XZR, which discards the result. So, instead of SUBS
// (aliased to CMP in this case) we issue SUB instead which writes the diff
// to the stack pointer; we assume you got x31 from NATIVE_STACK_POINTER.
let out: A64Opnd = out.into();
if let A64Opnd::Reg(A64Reg { reg_no: 31, .. }) = out {
sub(cb, out, left.into(), right.into());
} else {
subs(cb, out, left.into(), right.into());
}
},
Insn::Mul { left, right, out } => {
// If the next instruction is jo (jump on overflow)
Expand Down Expand Up @@ -1363,12 +1391,42 @@ mod tests {
asm.mov(sp, new_sp);
let new_sp = asm.sub(sp, 0x20.into());
asm.mov(sp, new_sp);
asm.compile_with_num_regs(&mut cb, 2);

assert_disasm!(cb, "e08300b11f000091e08300f11f000091", {"
asm.compile_with_num_regs(&mut cb, 2);
assert_disasm!(cb, "ff830091ff8300d1", "
0x0: add sp, sp, #0x20
0x4: sub sp, sp, #0x20
"});
");
}

#[test]
fn add_into() {
let (mut asm, mut cb) = setup_asm();

let sp = Opnd::Reg(XZR_REG);
asm.add_into(sp, 8.into());
asm.add_into(Opnd::Reg(X20_REG), 0x20.into());

asm.compile_with_num_regs(&mut cb, 0);
assert_disasm!(cb, "ff230091948200b1", "
0x0: add sp, sp, #8
0x4: adds x20, x20, #0x20
");
}

#[test]
fn sub_imm_reg() {
let (mut asm, mut cb) = setup_asm();

let difference = asm.sub(0x8.into(), Opnd::Reg(X5_REG));
asm.load_into(Opnd::Reg(X1_REG), difference);

asm.compile_with_num_regs(&mut cb, 1);
assert_disasm!(cb, "000180d2000005ebe10300aa", "
0x0: mov x0, #8
0x4: subs x0, x0, x5
0x8: mov x1, x0
");
}

#[test]
Expand Down
18 changes: 11 additions & 7 deletions zjit/zjit.mk
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,23 @@ zjit-bindgen: zjit.$(OBJEXT)
ZJIT_SRC_ROOT_PATH='$(top_srcdir)' BINDGEN_JIT_NAME=zjit $(CARGO) run --manifest-path '$(top_srcdir)/zjit/bindgen/Cargo.toml' -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS)
$(Q) if [ 'x$(HAVE_GIT)' = xyes ]; then $(GIT) -C "$(top_srcdir)" diff $(ZJIT_BINDGEN_DIFF_OPTS) zjit/src/cruby_bindings.inc.rs; fi

# Build env should roughly match what's used for miniruby to help with caching.
ZJIT_NEXTEST_ENV := RUBY_BUILD_DIR='$(TOP_BUILD_DIR)' \
RUBY_LD_FLAGS='$(LDFLAGS) $(XLDFLAGS) $(MAINLIBS)' \
MACOSX_DEPLOYMENT_TARGET=11.0 \
CARGO_TARGET_DIR='$(CARGO_TARGET_DIR)'

# We need `cargo nextest` for its one-process-per execution execution model
# since we can only boot the VM once per process. Normal `cargo test`
# runs tests in threads and can't handle this.
#
# On darwin, it's available through `brew install cargo-nextest`. See
# https://nexte.st/docs/installation/pre-built-binaries/ otherwise.
zjit-test: libminiruby.a
RUBY_BUILD_DIR='$(TOP_BUILD_DIR)' \
RUBY_LD_FLAGS='$(LDFLAGS) $(XLDFLAGS) $(MAINLIBS)' \
CARGO_TARGET_DIR='$(CARGO_TARGET_DIR)' \
$(CARGO) nextest run --manifest-path '$(top_srcdir)/zjit/Cargo.toml' $(ZJIT_TESTS)
$(ZJIT_NEXTEST_ENV) $(CARGO) nextest run \
--manifest-path '$(top_srcdir)/zjit/Cargo.toml' \
'--features=$(ZJIT_TEST_FEATURES)' \
$(ZJIT_TESTS)

# Run a ZJIT test written with Rust #[test] under LLDB
zjit-test-lldb: libminiruby.a
Expand All @@ -88,9 +94,7 @@ zjit-test-lldb: libminiruby.a
echo "Many tests only work when it's the only test in the process."; \
exit 1; \
fi; \
exe_path=`RUBY_BUILD_DIR='$(TOP_BUILD_DIR)' \
RUBY_LD_FLAGS='$(LDFLAGS) $(XLDFLAGS) $(MAINLIBS)' \
CARGO_TARGET_DIR='$(CARGO_TARGET_DIR)' \
exe_path=`$(ZJIT_NEXTEST_ENV) \
$(CARGO) nextest list --manifest-path '$(top_srcdir)/zjit/Cargo.toml' --message-format json --list-type=binaries-only | \
$(BASERUBY) -rjson -e 'puts JSON.load(STDIN.read).dig("rust-binaries", "zjit", "binary-path")'`; \
exec lldb $$exe_path -- --test-threads=1 $(ZJIT_TESTS)
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy