From 2552db04ddc44349c7b0f5f93aeb0fb02eccb509 Mon Sep 17 00:00:00 2001 From: himura467 Date: Thu, 14 May 2026 14:24:44 +0900 Subject: [PATCH 1/5] Fix UAF in IO::Buffer#& when self or mask is an invalidated slice --- io_buffer.c | 30 +++++++++++++++++++----------- test/ruby/test_io_buffer.rb | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 6dbc824e591799..c64a9b7fe7fbbd 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -3378,14 +3378,14 @@ io_buffer_pwrite(int argc, VALUE *argv, VALUE self) } static inline void -io_buffer_check_mask(const struct rb_io_buffer *buffer) +io_buffer_check_mask_size(size_t size) { - if (buffer->size == 0) + if (size == 0) rb_raise(rb_eIOBufferMaskError, "Zero-length mask given!"); } static void -memory_and(unsigned char * restrict output, unsigned char * restrict base, size_t size, unsigned char * restrict mask, size_t mask_size) +memory_and(unsigned char * restrict output, const unsigned char * restrict base, size_t size, const unsigned char * restrict mask, size_t mask_size) { for (size_t offset = 0; offset < size; offset += 1) { output[offset] = base[offset] & mask[offset % mask_size]; @@ -3413,13 +3413,21 @@ io_buffer_and(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + const void *base; + size_t size; + io_buffer_get_bytes_for_reading(buffer, &base, &size); - VALUE output = rb_io_buffer_new(NULL, buffer->size, io_flags_for_size(buffer->size)); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + + io_buffer_check_mask_size(mask_size); + + VALUE output = rb_io_buffer_new(NULL, size, io_flags_for_size(size)); struct rb_io_buffer *output_buffer = NULL; TypedData_Get_Struct(output, struct rb_io_buffer, &rb_io_buffer_type, output_buffer); - memory_and(output_buffer->base, buffer->base, buffer->size, mask_buffer->base, mask_buffer->size); + memory_and(output_buffer->base, base, size, mask_base, mask_size); return output; } @@ -3453,7 +3461,7 @@ io_buffer_or(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + io_buffer_check_mask_size(mask_buffer->size); VALUE output = rb_io_buffer_new(NULL, buffer->size, io_flags_for_size(buffer->size)); struct rb_io_buffer *output_buffer = NULL; @@ -3493,7 +3501,7 @@ io_buffer_xor(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + io_buffer_check_mask_size(mask_buffer->size); VALUE output = rb_io_buffer_new(NULL, buffer->size, io_flags_for_size(buffer->size)); struct rb_io_buffer *output_buffer = NULL; @@ -3590,7 +3598,7 @@ io_buffer_and_inplace(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + io_buffer_check_mask_size(mask_buffer->size); io_buffer_check_overlaps(buffer, mask_buffer); void *base; @@ -3636,7 +3644,7 @@ io_buffer_or_inplace(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + io_buffer_check_mask_size(mask_buffer->size); io_buffer_check_overlaps(buffer, mask_buffer); void *base; @@ -3682,7 +3690,7 @@ io_buffer_xor_inplace(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask(mask_buffer); + io_buffer_check_mask_size(mask_buffer->size); io_buffer_check_overlaps(buffer, mask_buffer); void *base; diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 6213bb28d72cbf..50eb1a08e3707e 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -702,6 +702,24 @@ def test_inplace_operators assert_equal IO::Buffer.for("\xce\xcd\xcc\xcb\xce\xcd\xcc\xcb\xce\xcd"), source.dup.not! end + def test_and_raises_on_freed_self + inner = IO::Buffer.new(IO::Buffer::PAGE_SIZE) + slice = inner.slice(0, 8) + inner.free + + mask = IO::Buffer.for("ABCDEFGH") + assert_raise(IO::Buffer::InvalidatedError) { slice & mask } + end + + def test_and_raises_on_freed_mask + inner = IO::Buffer.new(IO::Buffer::PAGE_SIZE) + mask_slice = inner.slice(0, 8) + inner.free + + source = IO::Buffer.for("ABCDEFGH") + assert_raise(IO::Buffer::InvalidatedError) { source & mask_slice } + end + def test_bit_count # All ones: 8 bits set per byte assert_equal 8, IO::Buffer.for("\xFF").bit_count From 57a02ad661abe11e69df7729eabb27550e5e4510 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 14 May 2026 18:35:08 -0700 Subject: [PATCH 2/5] ZJIT: Fix C-call preparation for backref/specialobject (#16974) --- zjit/src/codegen.rs | 26 +++++++++++++++------- zjit/src/codegen_tests.rs | 46 +++++++++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 8 +++---- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index eebd72c8727c5e..bbed16803f06fa 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -707,11 +707,11 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::SetIvar { self_val, id, ic, val, state } => no_output!(gen_setivar(jit, asm, opnd!(self_val), *id, *ic, opnd!(val), &function.frame_state(*state))), Insn::FixnumBitCheck { val, index } => gen_fixnum_bit_check(asm, opnd!(val), *index), Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, reason, *recompile, &function.frame_state(*state))), - Insn::PutSpecialObject { value_type } => gen_putspecialobject(asm, *value_type), + Insn::PutSpecialObject { value_type, state } => gen_putspecialobject(jit, asm, *value_type, &function.frame_state(*state)), Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state)), Insn::Defined { op_type, obj, pushval, v, lep_level, state } => gen_defined(jit, asm, *op_type, *obj, *pushval, opnd!(v), *lep_level, &function.frame_state(*state)), Insn::CheckMatch { target, pattern, flag, state } => gen_checkmatch(jit, asm, opnd!(target), opnd!(pattern), *flag, &function.frame_state(*state)), - Insn::GetSpecialSymbol { symbol_type, state: _ } => gen_getspecial_symbol(asm, *symbol_type), + Insn::GetSpecialSymbol { symbol_type, state } => gen_getspecial_symbol(asm, *symbol_type, &function.frame_state(*state)), Insn::GetSpecialNumber { nth, state } => gen_getspecial_number(asm, *nth, &function.frame_state(*state)), &Insn::IncrCounter(counter) => no_output!(gen_incr_counter(asm, counter)), Insn::IncrCounterPtr { counter_ptr } => no_output!(gen_incr_counter_ptr(asm, *counter_ptr)), @@ -1219,7 +1219,13 @@ fn gen_side_exit(jit: &mut JITState, asm: &mut Assembler, reason: &SideExitReaso } /// Emit a special object lookup -fn gen_putspecialobject(asm: &mut Assembler, value_type: SpecialObjectType) -> Opnd { +fn gen_putspecialobject(jit: &JITState, asm: &mut Assembler, value_type: SpecialObjectType, state: &FrameState) -> Opnd { + // rb_vm_get_special_object for CBASE/CONST_BASE can call rb_singleton_class, + // which allocates (may trigger GC) and can raise TypeError on non-class + // receivers (e.g. `123.instance_eval { Const = 1 }`). Treat as non-leaf so + // the PC is saved for GC and stack/locals are spilled for rescue. + gen_prepare_non_leaf_call(jit, asm, state); + // Get the EP of the current CFP and load it into a register let ep_opnd = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_EP); let ep_reg = asm.load(ep_opnd); @@ -1227,9 +1233,12 @@ fn gen_putspecialobject(asm: &mut Assembler, value_type: SpecialObjectType) -> O asm_ccall!(asm, rb_vm_get_special_object, ep_reg, Opnd::UImm(u64::from(value_type))) } -fn gen_getspecial_symbol(asm: &mut Assembler, symbol_type: SpecialBackrefSymbol) -> Opnd { - // Fetch a "special" backref based on the symbol type +fn gen_getspecial_symbol(asm: &mut Assembler, symbol_type: SpecialBackrefSymbol, state: &FrameState) -> Opnd { + // rb_backref_get reaches rb_vm_svar_lep, which calls CFP_PC/CFP_ISEQ on the + // current frame, so the PC must be saved before the call. + gen_prepare_leaf_call_with_gc(asm, state); + // Fetch a "special" backref based on the symbol type let backref = asm_ccall!(asm, rb_backref_get,); match symbol_type { @@ -1249,12 +1258,13 @@ fn gen_getspecial_symbol(asm: &mut Assembler, symbol_type: SpecialBackrefSymbol) } fn gen_getspecial_number(asm: &mut Assembler, nth: u64, state: &FrameState) -> Opnd { - // Fetch the N-th match from the last backref based on type shifted by 1 + // rb_backref_get reaches rb_vm_svar_lep, which calls CFP_PC/CFP_ISEQ on the + // current frame, so the PC must be saved before the call. + gen_prepare_leaf_call_with_gc(asm, state); + // Fetch the N-th match from the last backref based on type shifted by 1 let backref = asm_ccall!(asm, rb_backref_get,); - gen_prepare_leaf_call_with_gc(asm, state); - asm_ccall!(asm, rb_reg_nth_match, Opnd::Imm((nth >> 1).try_into().unwrap()), backref) } diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index fec8179bcb1fcd..fd6367b7a3ec81 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -4205,6 +4205,52 @@ fn test_getspecial_multiple_groups() { assert_snapshot!(assert_compiles(r#"test("123-456")"#), @r#""456""#); } +// In a JIT-to-JIT call, gen_push_frame writes JIT_RETURN_POISON to the +// callee's cfp->jit_return (runtime_checks builds). On the *first* such +// call the function stub trampoline clears jit_return to NULL, so the +// crash only manifests on the second JIT-to-JIT hit when the stub has +// been patched to jump directly to the callee's JIT entry. Putting $& as +// the first C call in the callee keeps the poison live until +// gen_getspecial_symbol calls rb_backref_get → rb_vm_svar_lep → CFP_PC → +// CFP_ZJIT_FRAME, which dereferences the poison without the prep fix. +#[test] +fn test_getspecial_symbol_in_jit_to_jit_callee() { + eval(r#" + def callee = $& + def caller_method = callee + + # Warm up callee so it JITs + callee + callee + + # First call to caller_method profiles; second JITs caller_method + # and runs through the function-stub-hit path which clears + # jit_return. The third call goes through the patched stub with + # POISON intact, hitting the bug. + caller_method + caller_method + "#); + assert_contains_opcode("callee", YARVINSN_getspecial); + assert_snapshot!(assert_compiles("caller_method"), @"nil"); +} + +// Same JIT-to-JIT setup, exercising gen_getspecial_number ($N). +#[test] +fn test_getspecial_number_in_jit_to_jit_callee() { + eval(r#" + def callee = $1 + def caller_method = callee + + callee + callee + + caller_method + caller_method + "#); + assert_contains_opcode("callee", YARVINSN_getspecial); + assert_snapshot!(assert_compiles("caller_method"), @"nil"); +} + #[test] fn test_profile_under_nested_jit_call() { assert_snapshot!(inspect(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 86bee5568a643f..e7ed3faf9c1050 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -856,7 +856,7 @@ pub enum Insn { ToRegexp { opt: usize, values: Vec, state: InsnId }, /// Put special object (VMCORE, CBASE, etc.) based on value_type - PutSpecialObject { value_type: SpecialObjectType }, + PutSpecialObject { value_type: SpecialObjectType, state: InsnId }, /// Call `to_a` on `val` if the method is defined, or make a new array `[val]` otherwise. ToArray { val: InsnId, state: InsnId }, @@ -1205,7 +1205,6 @@ macro_rules! for_each_operand_impl { | Insn::GetEP { .. } | Insn::LoadSelf | Insn::BreakPoint | Insn::Unreachable - | Insn::PutSpecialObject { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } => {} @@ -1222,6 +1221,7 @@ macro_rules! for_each_operand_impl { } Insn::PatchPoint { state, .. } | Insn::CheckInterrupts { state } + | Insn::PutSpecialObject { state, .. } | Insn::GetBlockParam { state, .. } | Insn::GetConstantPath { state, .. } => { $visit_one!(state); @@ -2224,7 +2224,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { write!(f, "SideExit {reason}") } } - Insn::PutSpecialObject { value_type } => write!(f, "PutSpecialObject {value_type}"), + Insn::PutSpecialObject { value_type, .. } => write!(f, "PutSpecialObject {value_type}"), Insn::Throw { throw_state, val, .. } => { write!(f, "Throw ")?; match throw_state & VM_THROW_STATE_MASK { @@ -6830,7 +6830,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let insn = if value_type == SpecialObjectType::VMCore { Insn::Const { val: Const::Value(unsafe { rb_mRubyVMFrozenCore }) } } else { - Insn::PutSpecialObject { value_type } + Insn::PutSpecialObject { value_type, state: exit_id } }; state.stack_push(fun.push_insn(block, insn)); } From b5038ac11837f340a49d45f6792c0fc3d688fed4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 02:13:06 +0000 Subject: [PATCH 3/5] 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.77.7 to 2.78.0 - [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/3235f8901fd37ffed0052b276cec25a362fb82e9...e1c4cd42111751368541a7cb5db3522bd1f846a4) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.78.0 dependency-type: direct:production update-type: version-update:semver-minor 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 18b6e9b38f8ca2..d39f89c7bd7952 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@3235f8901fd37ffed0052b276cec25a362fb82e9 # v2.77.7 + - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 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 5b4420d2452501..65ebdfa22cd4cd 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@3235f8901fd37ffed0052b276cec25a362fb82e9 # v2.77.7 + - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 9b747f5ef98669d571df1b73d5318bc09e4e13fd Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 14 May 2026 20:24:22 -0700 Subject: [PATCH 4/5] test_box.rb: extend timeout for Windows CI (#16963) test/ruby/test_box.rb: extend timeout for slow Windows CI The default 10-second assert_separately timeout is too tight for test_calling_root_box_methods_does_not_change_user_boxes_newly_created on Windows CI, where the subprocess completes successfully but slower than 10 seconds. --- test/ruby/test_box.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 34b5d5fa9623d5..a39979109fe909 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -899,7 +899,7 @@ def test_prelude_gems_and_loaded_features_with_disable_gems end def test_calling_root_box_methods_does_not_change_user_boxes_newly_created - assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true, timeout: 60) begin; assert_not_include Object.constants.sort, :Find # required by Pathname#find assert_not_include Ruby::Box.root.eval("Object.constants.sort"), :Find From 4d87d43b01dbb312eb1ff5fbbc6c9f33218d91a2 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Fri, 15 May 2026 13:15:40 +0800 Subject: [PATCH 5/5] Drop rbs require in rdoc-srcdir (#16983) This is currently blocking Ruby's doc generation: https://github.com/ruby/actions/actions/runs/25895616458/job/76107951475 We'll need to make rbs work for the process so we can adopt the rbs-integrated RDoc. But for now let's drop it to unblock doc generation. --- tool/rdoc-srcdir | 1 - 1 file changed, 1 deletion(-) diff --git a/tool/rdoc-srcdir b/tool/rdoc-srcdir index bc5e6c4e9e0020..417a057d7f4b88 100755 --- a/tool/rdoc-srcdir +++ b/tool/rdoc-srcdir @@ -2,7 +2,6 @@ require 'rubygems' require 'rdoc/rdoc' -require 'rbs' # Make only the output directory relative to the invoked directory. invoked = Dir.pwd