From 8c0eb987f14881bee0fce581cf392c3360acfba5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:10:25 +0000 Subject: [PATCH 1/4] Bump taiki-e/install-action Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.87.16 to 2.87.17 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/9114bf4d891761788c546334fd37538eae1bf8b3...94c31af3204a9f15ab40b35ad084410b905bbc73) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.87.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index fe0db1494ea8d4..585ae00adb8ae0 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3 # v2.87.16 + - uses: taiki-e/install-action@94c31af3204a9f15ab40b35ad084410b905bbc73 # v2.87.17 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index d80737229d0021..dfb381aa3e803d 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3 # v2.87.16 + - uses: taiki-e/install-action@94c31af3204a9f15ab40b35ad084410b905bbc73 # v2.87.17 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 27be6ccde45210efb99053bf4b30f663f3c659f4 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 22 Sep 2026 21:05:54 -0700 Subject: [PATCH 2/4] ZJIT: Avoid creating IseqPayload on rb_zjit_iseq_free (#18953) --- jit.c | 8 +++++ zjit/bindgen/src/main.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/gc.rs | 66 ++++++++++++++++++++++++++++++---- zjit/src/payload.rs | 5 +++ 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/jit.c b/jit.c index 5e26e1b87e1afa..0bc063eee9f98a 100644 --- a/jit.c +++ b/jit.c @@ -647,6 +647,14 @@ rb_iseq_set_jit_payload(const rb_iseq_t *iseq, void *payload) ISEQ_BODY(iseq)->jit_payload = payload; } +void +rb_iseq_clear_jit_payload(const rb_iseq_t *iseq) +{ + RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); + RUBY_ASSERT_ALWAYS(ISEQ_BODY(iseq)); + ISEQ_BODY(iseq)->jit_payload = NULL; +} + void rb_iseq_reset_jit_func(const rb_iseq_t *iseq) { diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index c4cb49cefd50c1..be4117def694d2 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -308,7 +308,7 @@ fn main() { .allowlist_type("rb_iseq_type") .allowlist_type("rb_event_flag_t") .allowlist_function("rb_object_shape_count") - .allowlist_function("rb_iseq_(get|set)_jit_payload") + .allowlist_function("rb_iseq_(get|set|clear)_jit_payload") .allowlist_function("rb_iseq_pc_at_idx") .allowlist_function("rb_iseq_opcode_at_pc") .allowlist_function("rb_iseq_bare_opcode_at_pc") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index dc0aa8698d362e..a3e76f21f5602f 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2597,6 +2597,7 @@ unsafe extern "C" { ); pub fn rb_iseq_get_jit_payload(iseq: *const rb_iseq_t) -> *mut ::std::os::raw::c_void; pub fn rb_iseq_set_jit_payload(iseq: *const rb_iseq_t, payload: *mut ::std::os::raw::c_void); + pub fn rb_iseq_clear_jit_payload(iseq: *const rb_iseq_t); pub fn rb_iseq_reset_jit_func(iseq: *const rb_iseq_t); pub fn rb_jit_get_page_size() -> u32; pub fn rb_jit_reserve_addr_space(mem_size: u32) -> *mut u8; diff --git a/zjit/src/gc.rs b/zjit/src/gc.rs index 7f5bc7891f20cd..ce9031f3f0da4d 100644 --- a/zjit/src/gc.rs +++ b/zjit/src/gc.rs @@ -3,7 +3,7 @@ use std::ptr::null; use std::{ffi::c_void, ops::Range}; use crate::{cruby::*, state::ZJITState, stats::with_time_stat, virtualmem::CodePtr}; -use crate::payload::{IseqPayload, IseqVersionRef, get_or_create_iseq_payload}; +use crate::payload::{IseqPayload, IseqVersionRef, get_iseq_payload_ptr}; use crate::stats::Counter::gc_time_ns; /// GC callback for marking GC objects in the per-ISEQ payload. @@ -51,14 +51,26 @@ pub extern "C" fn rb_zjit_iseq_free(iseq: IseqPtr) { return; } - // TODO(Shopify/ruby#682): Free `IseqPayload` - let payload = get_or_create_iseq_payload(iseq); - for version in payload.versions.iter_mut() { - unsafe { version.as_mut() }.iseq = null(); + ZJITState::get_invariants().forget_iseq(iseq); + + // If ZJIT has never created a payload for this ISEQ, do nothing. + let payload_ptr = get_iseq_payload_ptr(iseq); + if payload_ptr.is_null() { + return; } - let invariants = ZJITState::get_invariants(); - invariants.forget_iseq(iseq); + // Take ownership of the payload and unset it from the ISEQ. + let payload = unsafe { Box::from_raw(payload_ptr) }; + unsafe { rb_iseq_clear_jit_payload(iseq) }; + + // Clear IseqVersion references. Patch points may hold raw pointers to them, so + // they have to outlive the ISEQ. They're dropped when the assumption is broken. + for &version in payload.versions.iter() { + unsafe { (*version.as_ptr()).iseq = null() }; + } + + // Free the IseqPayload. + drop(payload); } /// GC callback for finalizing a CME @@ -242,3 +254,43 @@ pub extern "C" fn rb_zjit_root_mark() { unsafe { &*jit_frame }.mark(); } } + +#[cfg(test)] +mod tests { + use crate::cruby::test_utils::{eval, with_rubyvm}; + use crate::options::set_call_threshold; + + /// Regression test that freeing an ISEQ must not leave an `IseqPayload` behind. + #[test] + fn test_freed_iseqs_do_not_retain_payloads() { + with_rubyvm(|| { + // A high call threshold to keep any ISEQs out of the JIT + let old_call_threshold = unsafe { crate::options::rb_zjit_call_threshold }; + set_call_threshold(1_000_000); + + eval(r#" + def zjit_eval_iseqs(n) + n.times do |i| + eval("def __zjit_churn#{i}(x) = x + 1", TOPLEVEL_BINDING, "zjit_churn#{i}.rb") + send(:"__zjit_churn#{i}", i) + Object.send(:remove_method, :"__zjit_churn#{i}") + end + 3.times { GC.start } + end + "#); + + // Each iteration creates and frees several ISEQs. It should not grow the heap usage. + let alloc_bytes_after_eval = |n: usize| { + eval(&format!("zjit_eval_iseqs({n})")); + crate::stats::zjit_alloc_bytes() + }; + let small = alloc_bytes_after_eval(200); + let large = alloc_bytes_after_eval(2000); + + set_call_threshold(old_call_threshold); + + let alloc_growth = large.saturating_sub(small); + assert!(alloc_growth < 100_000, "zjit_alloc_bytes grew by {alloc_growth} bytes across freed ISEQs"); + }); + } +} diff --git a/zjit/src/payload.rs b/zjit/src/payload.rs index 07b9f351573c18..6d4a7bb20140c0 100644 --- a/zjit/src/payload.rs +++ b/zjit/src/payload.rs @@ -122,6 +122,11 @@ pub fn get_or_create_iseq_payload_ptr(iseq: IseqPtr) -> *mut IseqPayload { } } +/// Get a pointer to the payload object associated with an ISEQ, or null if never allocated. +pub fn get_iseq_payload_ptr(iseq: IseqPtr) -> *mut IseqPayload { + unsafe { rb_iseq_get_jit_payload(iseq) as *mut IseqPayload } +} + /// Get the payload object associated with an ISEQ. Create one if none exists. pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { let payload_non_null = get_or_create_iseq_payload_ptr(iseq); From 0fab30f723810af1608d2162e2aef32b44a1ee3a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 23 Sep 2026 06:47:43 +0200 Subject: [PATCH 3/4] compile.c: ensure swapped element isn't skipped [Bug #22377] When we statically resolve a `swap` we need to make sure the element we reordered will receive its optimization pass. --- compile.c | 13 ++++++++----- test/ruby/test_optimization.rb | 11 +++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/compile.c b/compile.c index be83950c55f1cb..472804dfb7508e 100644 --- a/compile.c +++ b/compile.c @@ -358,8 +358,6 @@ static void iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, const NOD #define IS_INSN_ID(iobj, insn) (INSN_OF(iobj) == BIN(insn)) #define IS_NEXT_INSN_ID(link, insn) \ ((link)->next && IS_INSN((link)->next) && IS_INSN_ID((link)->next, insn)) -#define IS_NEXT_NEXT_INSN_ID(link, insn) \ - ((link)->next && IS_NEXT_INSN_ID((link)->next, insn)) static inline bool IS_INDEPENDENT_INSN(LINK_ELEMENT *link) @@ -378,6 +376,10 @@ IS_INDEPENDENT_INSN(LINK_ELEMENT *link) type == BIN(duphash) || type == BIN(getinstancevariable) || type == BIN(getlocal) || + type == BIN(getlocal_WC_0) || + type == BIN(getlocal_WC_1) || + type == BIN(putobject_INT2FIX_0_) || + type == BIN(putobject_INT2FIX_1_) || type == BIN(opt_getconstant_path) ); } @@ -4326,10 +4328,11 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal * putnil / (or any other independent instruction) * putself / (or any other independent instruction) */ - if (IS_NEXT_NEXT_INSN_ID(&iobj->link, swap)) { - LINK_ELEMENT *first = &iobj->link; - LINK_ELEMENT *second = first->next; + if (IS_NEXT_INSN_ID(&iobj->link, swap)) { + LINK_ELEMENT *second = &iobj->link; + LINK_ELEMENT *first = second->prev; LINK_ELEMENT *swap = second->next; + if (IS_INDEPENDENT_INSN(first) && IS_INDEPENDENT_INSN(second)) { ELEM_REMOVE(swap); ELEM_SWAP(first, second); diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index c114b98e4792de..f7813b80998406 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -840,6 +840,17 @@ def freeze RUBY end + def test_peephole_static_swap + code = <<~'RUBY' + outer = nil + m = Module.new + outer::M = m + RUBY + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_no_match(/ getlocal\s/, insn, '[Bug #22377]') # unoptimized getlocal + end + def test_branch_condition_backquote bug = '[ruby-core:80740] [Bug #13444] redefined backquote should be called' class << self From 852b24345a05e01f32a10bb0a0e6cfb578f2d0ae Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 23 Sep 2026 09:41:05 +0900 Subject: [PATCH 4/4] Fix use-after-free in String#unpack If the source string is modified in the block of String#unpack, it can cause an use-after-free. For example, the following script crashes: fmt = "A" * 1_000_000 str = "C" * 1_000_000 str.unpack(fmt) { fmt.clear } --- pack.c | 8 ++++++++ test/ruby/test_pack.rb | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/pack.c b/pack.c index 16559f700e3678..e4fa33fc43d794 100644 --- a/pack.c +++ b/pack.c @@ -1108,6 +1108,8 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) long align_base; const char *sptr; long slen; + const char *fptr; + long flen; #define UNPACK_PUSH(item) do {\ VALUE item_val = (item);\ if ((mode) == UNPACK_BLOCK) {\ @@ -1116,6 +1118,10 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) if (RSTRING_PTR(str) != sptr || RSTRING_LEN(str) != slen) {\ rb_raise(rb_eRuntimeError, "string modified");\ }\ + /* The block may have also modified fmt and invalidated p */ \ + if (RSTRING_PTR(fmt) != fptr || RSTRING_LEN(fmt) != flen) {\ + rb_raise(rb_eRuntimeError, "format string modified");\ + }\ }\ else if ((mode) == UNPACK_ARRAY) {\ rb_ary_push(ary, item_val);\ @@ -1144,6 +1150,8 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) p = RSTRING_PTR(fmt); pend = p + RSTRING_LEN(fmt); + fptr = p; + flen = RSTRING_LEN(fmt); #define UNPACK_FETCH(var, type) (memcpy((var), s, sizeof(type)), s += sizeof(type)) diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 6eba3fb708c216..91b29d954a2fa9 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -259,6 +259,13 @@ def test_unpack_with_block_modifying_string } end + def test_unpack_with_block_modifying_format + fmt = "C" * 1_000_000 + assert_raise_with_message(RuntimeError, /format string modified/) { + ("A" * 4000).unpack(fmt) { fmt.clear } + } + end + def test_comment assert_equal("\0\1", [0,1].pack(" C #foo \n C ")) assert_equal([0,1], "\0\1".unpack(" C #foo \n C "))