From 1a12d846b5e7d76b6399eb423188937b754c4697 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 28 Jul 2026 09:30:20 -0400 Subject: [PATCH 01/14] Fix once insn when multiple Ractors race to execute the iseq The `once` insn (ex: for interpolated regexp with /o option) should only be executed once even across Ractors. The `once` value is associated with the iseq, which is shared across Ractors. Ideally, waiting threads in other Ractors would be put in a queue and signalled when the winning thread in another Ractor wins the race and executes the iseq successfully. For now, there's potential for busy looping when Ractors wait on each other. However, the bug is fixed and the iseq will only be executed once. --- test/ruby/test_ractor.rb | 15 +++++++++++++++ vm_insnhelper.c | 33 +++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 2b844f4ceff8ac..c60d973a994115 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -419,6 +419,21 @@ def test_detailed_message_in_ractor RUBY end + def test_ractor_vm_once_dispatch + assert_ractor(<<~'RUBY', args: ["-W0"], timeout: 30) + vals = 10.times.map do + Ractor.new { + a = nil + /#{sleep 0.1; a = "set"}/o + a + } + end.map(&:value) + vals.compact! + assert_equal 1, vals.size + assert_equal "set", vals.first + RUBY + end + def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index e7acfd8a795770..67606b933ae92a 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -5503,7 +5503,7 @@ static VALUE vm_once_clear(VALUE data) { union iseq_inline_storage_entry *is = (union iseq_inline_storage_entry *)data; - is->once.running_thread = NULL; + rbimpl_atomic_ptr_store((volatile void **)&is->once.running_thread, NULL, RBIMPL_ATOMIC_RELEASE); return Qnil; } @@ -6606,33 +6606,46 @@ vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) { rb_thread_t *th = rb_ec_thread_ptr(ec); rb_thread_t *const RUNNING_THREAD_ONCE_DONE = (rb_thread_t *)(0x1); + rb_thread_t *running_th; again: - if (is->once.running_thread == RUNNING_THREAD_ONCE_DONE) { + running_th = rbimpl_atomic_ptr_load((void**)&is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th == RUNNING_THREAD_ONCE_DONE) { return is->once.value; } - else if (is->once.running_thread == NULL) { - VALUE val; - is->once.running_thread = th; - val = rb_ensure(vm_once_exec, (VALUE)iseq, vm_once_clear, (VALUE)is); - // TODO: confirm that it is shareable + else if (running_th == NULL) { + VALUE val = Qundef; + enum ruby_tag_type state; + if (rbimpl_atomic_ptr_cas((void**)&is->once.running_thread, running_th, th, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED) != running_th) { + goto again; + } + EC_PUSH_TAG(ec); + if ((state = EC_EXEC_TAG()) == TAG_NONE) { + val = vm_once_exec((VALUE)iseq); + } + EC_POP_TAG(); + if (state != TAG_NONE) { + vm_once_clear((VALUE)is); + EC_JUMP_TAG(ec, state); + } + // TODO: confirm that it is shareable if (RB_FL_ABLE(val)) { RB_OBJ_SET_SHAREABLE(val); } RB_OBJ_WRITE(CFP_ISEQ(ec->cfp), &is->once.value, val); - /* is->once.running_thread is cleared by vm_once_clear() */ - is->once.running_thread = RUNNING_THREAD_ONCE_DONE; /* success */ + rbimpl_atomic_ptr_store((volatile void**)&is->once.running_thread, RUNNING_THREAD_ONCE_DONE, RBIMPL_ATOMIC_RELEASE); /* success */ return val; } - else if (is->once.running_thread == th) { + else if (running_th == th) { /* recursive once */ return vm_once_exec((VALUE)iseq); } else { /* waiting for finish */ + /* NOTE: this is not ideal if we're the only thread in the Ractor (it's a busy loop!) */ RUBY_VM_CHECK_INTS(ec); rb_thread_schedule(); goto again; From 67eb41e051916d269d83da4e6f20b7bb2ceae3d5 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 28 Jul 2026 10:25:11 -0400 Subject: [PATCH 02/14] Fix cross-ractor busy-waiting in vm_once_dispatch Use a proper nogvl + native mutex broadcast solution to deal with cross-ractor thread synchronization. --- thread.c | 4 ++++ vm.c | 2 ++ vm_core.h | 4 ++++ vm_insnhelper.c | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/thread.c b/thread.c index 3b32ddb2cf8a6c..e07e1cda3a57e7 100644 --- a/thread.c +++ b/thread.c @@ -5083,6 +5083,8 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r /* may be held by any thread in parent */ rb_native_mutex_initialize(&th->interrupt_lock); + rb_native_mutex_initialize(&vm->once_lock); + rb_native_cond_initialize(&vm->once_cond); ccan_list_head_init(&th->interrupt_exec_tasks); vm->fork_gen++; @@ -5755,6 +5757,8 @@ Init_Thread_Mutex(void) rb_thread_t *th = GET_THREAD(); rb_native_mutex_initialize(&th->vm->workqueue_lock); + rb_native_mutex_initialize(&th->vm->once_lock); + rb_native_cond_initialize(&th->vm->once_cond); rb_native_mutex_initialize(&th->interrupt_lock); } diff --git a/vm.c b/vm.c index 213b396bc09e50..d95fab6b0810db 100644 --- a/vm.c +++ b/vm.c @@ -3566,6 +3566,8 @@ ruby_vm_destruct(rb_vm_t *vm) rb_objspace_free(objspace); } rb_native_mutex_destroy(&vm->workqueue_lock); + rb_native_mutex_destroy(&vm->once_lock); + rb_native_cond_destroy(&vm->once_cond); /* after freeing objspace, you *can't* use ruby_xfree() */ ruby_current_vm_ptr = NULL; diff --git a/vm_core.h b/vm_core.h index 0ca0598678edc2..73546f7642e564 100644 --- a/vm_core.h +++ b/vm_core.h @@ -802,6 +802,10 @@ typedef struct rb_vm_struct { struct ccan_list_head workqueue; /* <=> rb_workqueue_job.jnode */ rb_nativethread_lock_t workqueue_lock; + /* `once` completion event (see vm_once_dispatch) */ + rb_nativethread_lock_t once_lock; + rb_nativethread_cond_t once_cond; + VALUE orig_progname, progname; VALUE coverages, me2counter; int coverage_mode; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 67606b933ae92a..ce27c435c3c67e 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -28,6 +28,7 @@ #include "internal/variable.h" #include "internal/set_table.h" #include "internal/struct.h" +#include "ruby/thread.h" #include "variable.h" /* finish iseq array */ @@ -5499,11 +5500,49 @@ vm_once_exec(VALUE iseq) return rb_proc_call_with_block(proc, 0, 0, Qnil); } +#define RUNNING_THREAD_ONCE_DONE ((rb_thread_t *)0x1) + +struct vm_once_wait_arg { + rb_vm_t *vm; + union iseq_inline_storage_entry *is; +}; + +static void +vm_once_broadcast(rb_vm_t *vm) +{ + rb_native_mutex_lock(&vm->once_lock); + rb_native_cond_broadcast(&vm->once_cond); + rb_native_mutex_unlock(&vm->once_lock); +} + +static void * +vm_once_wait_no_gvl(void *ptr) +{ + struct vm_once_wait_arg *arg = (struct vm_once_wait_arg *)ptr; + rb_vm_t *vm = arg->vm; + rb_thread_t *running_th; + + rb_native_mutex_lock(&vm->once_lock); + running_th = rbimpl_atomic_ptr_load((void **)&arg->is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th != NULL && running_th != RUNNING_THREAD_ONCE_DONE) { + rb_native_cond_wait(&vm->once_cond, &vm->once_lock); + } + rb_native_mutex_unlock(&vm->once_lock); + return NULL; +} + +static void +vm_once_wait_ubf(void *ptr) +{ + vm_once_broadcast((rb_vm_t *)ptr); +} + static VALUE vm_once_clear(VALUE data) { union iseq_inline_storage_entry *is = (union iseq_inline_storage_entry *)data; rbimpl_atomic_ptr_store((volatile void **)&is->once.running_thread, NULL, RBIMPL_ATOMIC_RELEASE); + vm_once_broadcast(GET_VM()); return Qnil; } @@ -6605,7 +6644,6 @@ static VALUE vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) { rb_thread_t *th = rb_ec_thread_ptr(ec); - rb_thread_t *const RUNNING_THREAD_ONCE_DONE = (rb_thread_t *)(0x1); rb_thread_t *running_th; again: @@ -6637,6 +6675,7 @@ vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) RB_OBJ_WRITE(CFP_ISEQ(ec->cfp), &is->once.value, val); rbimpl_atomic_ptr_store((volatile void**)&is->once.running_thread, RUNNING_THREAD_ONCE_DONE, RBIMPL_ATOMIC_RELEASE); /* success */ + vm_once_broadcast(rb_ec_vm_ptr(ec)); return val; } else if (running_th == th) { @@ -6644,10 +6683,11 @@ vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) return vm_once_exec((VALUE)iseq); } else { - /* waiting for finish */ - /* NOTE: this is not ideal if we're the only thread in the Ractor (it's a busy loop!) */ + /* wait for the winner to finish */ + struct vm_once_wait_arg arg = { .vm = rb_ec_vm_ptr(ec), .is = is }; + rb_nogvl(vm_once_wait_no_gvl, &arg, vm_once_wait_ubf, arg.vm, + RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_INTR_FAIL); RUBY_VM_CHECK_INTS(ec); - rb_thread_schedule(); goto again; } } From 7a4de4748a11c380323d9c5f52b7f9d44b5d2f6b Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 29 Jul 2026 03:28:43 +0900 Subject: [PATCH 03/14] [ruby/openssl] pkcs7: fix flaky test test_enveloped OpenSSL::PKCS7#decrypt is unreliable when given only a private key and the PKCS#7 structure contains multiple recipients. It may occasionally raise an exception or return garbage data. Update the test to avoid this. This became much more visible in CI after upgrading to OpenSSL versions released on 2026-06-09, which enable the implicit rejection mechanism for PKCS#1 v1.5 decryption in PKCS#7. https://github.com/ruby/openssl/commit/f7c51a2c0c --- test/openssl/test_pkcs7.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/openssl/test_pkcs7.rb b/test/openssl/test_pkcs7.rb index b3129c0cdfe610..c677d8223bdb21 100644 --- a/test/openssl/test_pkcs7.rb +++ b/test/openssl/test_pkcs7.rb @@ -238,8 +238,6 @@ def test_enveloped assert_equal(@ee2_cert.serial, recip[1].serial) assert_equal(data, p7.decrypt(@ee2_key, @ee2_cert)) - assert_equal(data, p7.decrypt(@ee1_key)) - assert_raise(OpenSSL::PKCS7::PKCS7Error) { p7.decrypt(@ca_key, @ca_cert) } @@ -250,6 +248,16 @@ def test_enveloped } end + def test_enveloped_decrypt_key_only + omit_on_fips # PKCS #1 v1.5 padding + + data = "aaaaa\nbbbbb\nccccc\n" + cipher = "AES-128-CBC" + + p7 = OpenSSL::PKCS7.encrypt([@ee1_cert], data, cipher, OpenSSL::PKCS7::BINARY) + assert_equal(data, p7.decrypt(@ee1_key)) + end + def test_enveloped_add_recipient omit_on_fips # PKCS #1 v1.5 padding From 120c8fa88c222636f5e292224d3c8fba0ef254a8 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 28 Jul 2026 15:11:47 -0400 Subject: [PATCH 04/14] Fix memory leak when sweeping hashes with st_table The GC sweep fast path had an inverted condition for hashes in rb_gc_obj_needs_cleanup_p. Hashes with an st_table needs to be freed. The following script leaks memory: 10.times do 100_000.times do # Hashes with more than 8 elements use st_table with a malloc'd # entries array h = {} 9.times { |i| h[i] = i } end GC.start puts `ps -o rss= -p #{$$}` end Before: 58816 97056 135296 173584 211840 250096 288336 326608 364848 403088 After: 27696 27696 27696 27696 27696 27696 27696 27696 27696 27696 --- gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gc.c b/gc.c index 6ab512d468b4bc..522746ecad44fd 100644 --- a/gc.c +++ b/gc.c @@ -1453,7 +1453,7 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) return !(flags & RARRAY_EMBED_FLAG); case T_HASH: - return !(flags & RHASH_ST_TABLE_FLAG); + return (flags & RHASH_ST_TABLE_FLAG); case T_MATCH: return !((flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER); From c05f55416f1e44c4a8b617b187bbfc5b34c9a00a Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 28 Jul 2026 16:34:46 -0400 Subject: [PATCH 05/14] Remove flaky assertion in test_thread_timer_and_interrupt Don't rely on exact timings even if they seem generous enough. This has failed recently on CI (MacOS-15-intel). --- test/ruby/test_thread.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 9f509cc4f220c1..42eb44f4a6122f 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1016,11 +1016,10 @@ def test_thread_timer_and_interrupt cmd = 'Signal.trap(:INT, "DEFAULT"); pipe=IO.pipe; Thread.start {Thread.pass until Thread.main.stop?; puts; STDOUT.flush}; pipe[0].read' opt = {} opt[:new_pgroup] = true if /mswin|mingw/ =~ RUBY_PLATFORM - s, t, _err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, **opt) do |in_p, out_p, err_p, cpid| + s, err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, **opt) do |in_p, out_p, err_p, cpid| assert IO.select([out_p], nil, nil, 10), 'subprocess not ready' out_p.gets pid = cpid - t0 = Time.now.to_f Process.kill(:SIGINT, pid) begin Timeout.timeout(10) { Process.wait(pid) } @@ -1028,14 +1027,12 @@ def test_thread_timer_and_interrupt EnvUtil.terminate(pid) raise end - t1 = Time.now.to_f - [$?, t1 - t0, err_p.read] + [$?, err_p.read] end assert_equal(pid, s.pid, bug5757) assert_equal([false, true, false, Signal.list["INT"]], [s.exited?, s.signaled?, s.stopped?, s.termsig], "[s.exited?, s.signaled?, s.stopped?, s.termsig]") - assert_include(0..2, t, bug5757) end def test_thread_join_in_trap From cf81909161c9677e18dac7b9f93df0035415eef9 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 28 Jul 2026 14:31:02 -0700 Subject: [PATCH 06/14] Reapply "ZJIT: Add counters for throw and exception_handler" (#17945) (#18099) This reverts commit ff6ac97529962b824d528b4bf32485179bb25805. --- zjit/src/codegen.rs | 24 +++++++++++++++++++++--- zjit/src/hir.rs | 1 + zjit/src/stats.rs | 12 +++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index da6283fb0b5ee8..51b488b9e8a51f 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -213,7 +213,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) -> Result { // We don't support exception handlers yet if jit_exception { - return Err(CompileError::ExceptionHandler); + return gen_exception_handler_counter(cb); } let iseq_name = iseq_get_location(iseq, 0); @@ -525,7 +525,6 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func debug!("ZJIT: gen_function: Failed to compile insn: {insn_id} {insn}. Generating side-exit."); gen_incr_counter(&mut asm, exit_counter_for_unhandled_hir_insn(&insn)); let reason = match insn { - Insn::Throw { .. } => SideExitReason::UnhandledHIRThrow, Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin, _ => SideExitReason::UnhandledHIRUnknown(insn_id), }; @@ -784,7 +783,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IsA { val, class } => gen_is_a(jit, asm, opnd!(val), opnd!(class)), &Insn::ArrayMax { ref elements, state } => gen_array_max(jit, asm, function, opnds!(elements), &function.frame_state(state)), &Insn::ArrayMin { ref elements, state } => gen_array_min(jit, asm, function, opnds!(elements), &function.frame_state(state)), - &Insn::Throw { state, .. } => return Err(state), + &Insn::Throw { state, .. } => no_output!(gen_throw(jit, asm, function, &function.frame_state(state))), &Insn::CondBranch { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), }; @@ -2652,6 +2651,12 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) { asm.cret(C_RET_OPND); } +fn gen_throw(jit: &mut JITState, asm: &mut Assembler, function: &Function, state: &FrameState) { + // TODO: Consider calling rb_vm_throw and propagating ec->tag->state to the interpreter. + // Also consider making it a jump on method inlining. + gen_side_exit(jit, asm, function, &SideExitReason::Throw, None, state); +} + /// Compile Fixnum + Fixnum fn gen_fixnum_add(jit: &mut JITState, asm: &mut Assembler, function: &Function, left: lir::Opnd, right: lir::Opnd, state: &FrameState) -> lir::Opnd { // Add left + right and test for overflow @@ -4041,6 +4046,19 @@ fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) - }) } +/// Generate a JIT entry that just increments exit_exception_handler and exits +fn gen_exception_handler_counter(cb: &mut CodeBlock) -> Result { + let mut asm = Assembler::new(); + asm.new_block_without_id("exception_handler_counter"); + gen_incr_counter(&mut asm, Counter::exit_exception_handler); + asm.cret(Qundef.into()); + + asm.compile(cb).map(|(code_ptr, gc_offsets)| { + assert_eq!(0, gc_offsets.len()); + code_ptr + }) +} + /// Given the number of spill slots needed for a function, return the number of bytes /// the function needs to allocate on the stack for the stack frame. fn aligned_stack_bytes(num_slots: usize) -> usize { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 21c1c701cf0897..d0a1bb270e3c48 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -553,6 +553,7 @@ pub enum SideExitReason { PatchPoint(Invariant), CalleeSideExit, Interrupt, + Throw, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, BlockParamProxyNotProc, diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 8a0c55c8d58667..7ebfb62309a0d9 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -185,6 +185,7 @@ make_counters! { exit { // exit_: Side exits reasons exit_compile_error, + exit_exception_handler, exit_unhandled_newarray_send_min, exit_unhandled_newarray_send_hash, exit_unhandled_newarray_send_pack, @@ -226,6 +227,7 @@ make_counters! { exit_patchpoint_root_box_only, exit_callee_side_exit, exit_interrupt, + exit_throw, exit_stackoverflow, exit_block_param_proxy_not_iseq_or_ifunc, exit_block_param_proxy_not_nil, @@ -349,7 +351,6 @@ make_counters! { compile_error_iseq_version_limit_reached, compile_error_iseq_stack_too_large, compile_error_native_stack_too_large, - compile_error_exception_handler, compile_error_out_of_memory, compile_error_label_linking_failure, compile_error_jit_to_jit_optional, @@ -369,9 +370,6 @@ make_counters! { compile_error_validation_misc_validation_error, // unhandled_hir_insn_: Unhandled HIR instructions - unhandled_hir_insn_array_max, - unhandled_hir_insn_fixnum_div, - unhandled_hir_insn_throw, unhandled_hir_insn_invokebuiltin, unhandled_hir_insn_unknown, @@ -525,7 +523,6 @@ pub enum CompileError { IseqVersionLimitReached, IseqStackTooLarge, NativeStackTooLarge, - ExceptionHandler, OutOfMemory, ParseError(ParseError), /// When a ZJIT function is too large, the branches may have @@ -544,7 +541,6 @@ pub fn exit_counter_for_compile_error(compile_error: &CompileError) -> Counter { IseqVersionLimitReached => compile_error_iseq_version_limit_reached, IseqStackTooLarge => compile_error_iseq_stack_too_large, NativeStackTooLarge => compile_error_native_stack_too_large, - ExceptionHandler => compile_error_exception_handler, OutOfMemory => compile_error_out_of_memory, LabelLinkingFailure => compile_error_label_linking_failure, ParseError(parse_error) => match parse_error { @@ -570,9 +566,6 @@ pub fn exit_counter_for_unhandled_hir_insn(insn: &crate::hir::Insn) -> Counter { use crate::hir::Insn::*; use crate::stats::Counter::*; match insn { - ArrayMax { .. } => unhandled_hir_insn_array_max, - FixnumDiv { .. } => unhandled_hir_insn_fixnum_div, - Throw { .. } => unhandled_hir_insn_throw, InvokeBuiltin { .. } => unhandled_hir_insn_invokebuiltin, _ => unhandled_hir_insn_unknown, } @@ -619,6 +612,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardSuperMethodEntry => exit_guard_super_method_entry, CalleeSideExit => exit_callee_side_exit, Interrupt => exit_interrupt, + Throw => exit_throw, StackOverflow => exit_stackoverflow, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, BlockParamProxyNotNil => exit_block_param_proxy_not_nil, From 2999c81ce24b37f891a51f29af88ae255588b079 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 28 Jul 2026 15:39:55 -0400 Subject: [PATCH 07/14] Fix memory leak in MatchData The following script leaks memory: 10.times do # MatchData#offset allocates an external char_offset buffer 100_000.times.map do m = /(a)(b)(c)(d)/.match("abcd") m.offset(0) m end GC.start puts `ps -o rss= -p #{$$}` end Before: 49072 56512 66320 72624 80432 89232 96192 105600 111936 121744 After: 49328 48928 50864 49312 49248 50160 49264 50816 49296 51216 --- gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gc.c b/gc.c index 522746ecad44fd..be3d82d46f13ee 100644 --- a/gc.c +++ b/gc.c @@ -1456,7 +1456,7 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) return (flags & RHASH_ST_TABLE_FLAG); case T_MATCH: - return !((flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER); + return (flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER; case T_BIGNUM: return !(flags & BIGNUM_EMBED_FLAG); From d13a49fc50be54f01488932b27679b94028fef62 Mon Sep 17 00:00:00 2001 From: Daichi Kamiyama <32436625+dak2@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:57:20 +0900 Subject: [PATCH 08/14] ZJIT: Fix kwargs handling in invokeblock ifunc specialization (#17971) gen_invokeblock_ifunc drops the callinfo and passes argv straight to rb_vm_yield_with_cfunc, which hardcodes kw_splat = 0, so keyword arguments lost their keyword-ness. Gate the ifunc specialization on block_call_inlinable, the same check used for the inline ISEQ path. --- zjit/src/codegen_tests.rs | 12 ++++++++++++ zjit/src/hir.rs | 2 +- zjit/src/hir/opt_tests.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index d03bd2d65e52e3..a943f06d572400 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -6012,6 +6012,18 @@ fn test_invokeblock_ifunc_map() { assert_snapshot!(assert_compiles("test"), @"[2, 4, 6]"); } +#[test] +fn test_invokeblock_ifunc_kwarg() { + eval(" + def foo + yield 1, a: 2 + end + def test = enum_for(:foo).to_a + test + "); + assert_snapshot!(assert_compiles("test"), @"[[1, {a: 2}]]"); +} + #[test] fn test_ccall_variadic_with_multiple_args() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d0a1bb270e3c48..6d3fa90f10599e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9326,7 +9326,7 @@ fn add_iseq_to_hir( Some(summary.bucket(0).class()) }); - let is_ifunc = (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT)) == 0 + let is_ifunc = (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_KWARG)) == 0 && block_handler_class.is_some_and(|obj| unsafe { rb_IMEMO_TYPE_P(obj, imemo_ifunc) == 1 }); // If the block handler is a known simple ISEQ block with exact arity and no diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 1682fe23eca4bb..4d50dc51772d97 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -17913,6 +17913,34 @@ mod hir_opt_tests { "); } + #[test] + fn test_invokeblock_ifunc_kwarg() { + eval(" + def foo + yield 1, a: 2 + end + def test = enum_for(:foo).to_a + test + "); + assert_snapshot!(hir_string("foo"), @" + fn foo@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[1] = Const Value(1) + v12:Fixnum[2] = Const Value(2) + v14:BasicObject = InvokeBlock v10, v12 # SendFallbackReason: InvokeBlock: not yet specialized + CheckInterrupts + Return v14 + "); + } + #[test] fn test_dedup_guard_type() { // Two subtractions on the same Fixnum operand `n` each require a From dd1528212d6860631ab46c86051efa6a68b0482c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:07:51 +0000 Subject: [PATCH 09/14] 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.85.2 to 2.85.3 - [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/41049aa56687c35e0afa74eed4f09cec4f9afabf...18b1216eba7f8039b0f8d131d5473787f0edce68) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.85.3 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 9db644da16f9d3..6118906823e8e0 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@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2.85.2 + - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 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 8e72f7d4835947..0843385bce8346 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -141,7 +141,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2.85.2 + - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 2d616a9bb0ff2f2c0adc8542367f1d102f290141 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 29 Jul 2026 10:21:10 +0900 Subject: [PATCH 10/14] Remove unused autoload table copier Remove the copier left unused when class fields cloning replaced raw table copying in commit 3abdd4241fd5. --- internal/variable.h | 1 - variable.c | 64 --------------------------------------------- 2 files changed, 65 deletions(-) diff --git a/internal/variable.h b/internal/variable.h index 461676e6fae300..53d991c2be131b 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -22,7 +22,6 @@ VALUE rb_search_class_path(VALUE); VALUE rb_attr_delete(VALUE, ID); void rb_autoload_str(VALUE mod, ID id, VALUE file); VALUE rb_autoload_at_p(VALUE, ID, int); -void rb_autoload_copy_table_for_box(st_table *, const rb_box_t *); NORETURN(VALUE rb_mod_const_missing(VALUE,VALUE)); rb_gvar_getter_t *rb_gvar_getter_function_of(ID); rb_gvar_setter_t *rb_gvar_setter_function_of(ID); diff --git a/variable.c b/variable.c index e0b4d4a45db2a7..a0f24c0868dd55 100644 --- a/variable.c +++ b/variable.c @@ -2687,70 +2687,6 @@ get_autoload_data(VALUE autoload_const_value, struct autoload_const **autoload_c return autoload_data; } -struct autoload_copy_table_data { - VALUE dst_tbl_value; - struct st_table *dst_tbl; - const rb_box_t *box; -}; - -static int -autoload_copy_table_for_box_i(st_data_t key, st_data_t value, st_data_t arg) -{ - struct autoload_const *autoload_const; - struct autoload_copy_table_data *data = (struct autoload_copy_table_data *)arg; - struct st_table *tbl = data->dst_tbl; - VALUE tbl_value = data->dst_tbl_value; - const rb_box_t *box = data->box; - - VALUE src_value = (VALUE)value; - struct autoload_const *src_const = rb_check_typeddata(src_value, &autoload_const_type); - // autoload_data can be shared between copies because the feature is equal between copies. - VALUE autoload_data_value = src_const->autoload_data_value; - struct autoload_data *autoload_data = rb_check_typeddata(autoload_data_value, &autoload_data_type); - - VALUE new_value = TypedData_Make_Struct(0, struct autoload_const, &autoload_const_type, autoload_const); - RB_OBJ_WRITE(new_value, &autoload_const->box_value, rb_get_box_object((rb_box_t *)box)); - RB_OBJ_WRITE(new_value, &autoload_const->module, src_const->module); - autoload_const->name = src_const->name; - RB_OBJ_WRITE(new_value, &autoload_const->value, src_const->value); - autoload_const->flag = src_const->flag; - RB_OBJ_WRITE(new_value, &autoload_const->autoload_data_value, autoload_data_value); - ccan_list_add_tail(&autoload_data->constants, &autoload_const->cnode); - - st_insert(tbl, (st_data_t)autoload_const->name, (st_data_t)new_value); - RB_OBJ_WRITTEN(tbl_value, Qundef, new_value); - - return ST_CONTINUE; -} - -void -rb_autoload_copy_table_for_box(st_table *iv_ptr, const rb_box_t *box) -{ - struct st_table *src_tbl, *dst_tbl; - VALUE src_tbl_value, dst_tbl_value; - if (!rb_st_lookup(iv_ptr, (st_data_t)autoload, (st_data_t *)&src_tbl_value)) { - // the class has no autoload table yet. - return; - } - if (!RTEST(src_tbl_value) || !(src_tbl = check_autoload_table(src_tbl_value))) { - // the __autoload__ ivar value isn't autoload table value. - return; - } - src_tbl = check_autoload_table(src_tbl_value); - - dst_tbl_value = TypedData_Wrap_Struct(0, &autoload_table_type, NULL); - RTYPEDDATA_DATA(dst_tbl_value) = dst_tbl = st_init_numtable(); - - struct autoload_copy_table_data data = { - .dst_tbl_value = dst_tbl_value, - .dst_tbl = dst_tbl, - .box = box, - }; - - st_foreach(src_tbl, autoload_copy_table_for_box_i, (st_data_t)&data); - st_insert(iv_ptr, (st_data_t)autoload, (st_data_t)dst_tbl_value); -} - void rb_autoload(VALUE module, ID name, const char *feature) { From 011591a0696a653894ff704f75f0f0e4434ac46d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 16:27:39 +0900 Subject: [PATCH 11/14] [ruby/rubygems] Add --cooldown flag to bundle lock and bundle cache Both commands resolve, but only install, update, add and outdated accepted the flag, so the resolver's failure hint suggesting `--cooldown 0` was not actionable on `bundle lock --update`. https://github.com/ruby/rubygems/commit/2c4f4f390a Co-Authored-By: Claude Fable 5 --- lib/bundler/cli.rb | 2 ++ lib/bundler/cli/lock.rb | 3 ++ lib/bundler/man/bundle-cache.1 | 3 ++ lib/bundler/man/bundle-cache.1.ronn | 6 ++++ lib/bundler/man/bundle-lock.1 | 5 ++- lib/bundler/man/bundle-lock.1.ronn | 6 ++++ spec/bundler/install/cooldown_spec.rb | 51 +++++++++++++++++++++++++++ 7 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index e15e708f7777c6..783469e56bf240 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -462,6 +462,7 @@ def fund method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)." method_option "quiet", type: :boolean, banner: "Only output warnings and errors." method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this bundle cache operation's install (removed)" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." long_desc <<-D The cache command will copy the .gem files for every gem in the bundle into the directory ./vendor/cache. If you then check that directory into your source @@ -643,6 +644,7 @@ def inject(*) method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def lock require_relative "cli/lock" Lock.new(options).run diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index 2f78868936c108..f6732c59254a09 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -16,6 +16,9 @@ def run check_for_conflicting_options + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + print = options[:print] previous_output_stream = Bundler.ui.output_stream Bundler.ui.output_stream = :stderr if print diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index 38ea0479612404..81d4a9e04265d6 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -17,6 +17,9 @@ Include gems for all platforms present in the lockfile, not only the current one \fB\-\-cache\-path=CACHE\-PATH\fR Specify a different cache path than the default (vendor/cache)\. .TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving before caching\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. +.TP \fB\-\-gemfile=GEMFILE\fR Use the specified gemfile instead of Gemfile\. .TP diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn index 51846c96b42a25..91e16cde22844c 100644 --- a/lib/bundler/man/bundle-cache.1.ronn +++ b/lib/bundler/man/bundle-cache.1.ronn @@ -21,6 +21,12 @@ use the gems in the cache in preference to the ones on `rubygems.org`. * `--cache-path=CACHE-PATH`: Specify a different cache path than the default (vendor/cache). +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving before caching. Pass `0` to disable cooldown for this run, + overriding any per-source or global configuration. See `cooldown` in + bundle-config(1). + * `--gemfile=GEMFILE`: Use the specified gemfile instead of Gemfile. diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index 396c8ff6ca9766..8f6a77e435eef6 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" -\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] +\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] [\-\-cooldown=NUMBER] .SH "DESCRIPTION" Lock the gems specified in Gemfile\. .SH "OPTIONS" @@ -59,6 +59,9 @@ If updating, do not allow any gem to be updated past latest \-\-patch | \-\-mino .TP \fB\-\-conservative\fR If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\. +.TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. .SH "UPDATING ALL GEMS" If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. .SH "UPDATING A LIST OF GEMS" diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn index 6d3e63c982c500..683a429b0e1ef7 100644 --- a/lib/bundler/man/bundle-lock.1.ronn +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -20,6 +20,7 @@ bundle-lock(1) -- Creates / Updates a lockfile without installing [--pre] [--strict] [--conservative] + [--cooldown=NUMBER] ## DESCRIPTION @@ -84,6 +85,11 @@ Lock the gems specified in Gemfile. * `--conservative`: If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated. +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. See `cooldown` in bundle-config(1). + ## UPDATING ALL GEMS If you run `bundle lock` with `--update` option without list of gems, bundler will diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 8d42f4ad8255eb..e266d501923b37 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -712,6 +712,57 @@ expect(lockfile).not_to include("ripe_gem (2.0.0)") end + it "applies CLI --cooldown on bundle lock --update" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update --cooldown 7", artifice: "compact_index_cooldown" + + expect(lockfile).to include("ripe_gem (1.0.0)") + expect(lockfile).not_to include("ripe_gem (2.0.0)") + end + + it "rejects a negative --cooldown value on bundle lock" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "lock --cooldown=-7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end + + it "applies CLI --cooldown on bundle cache" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "cache --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + expect(bundled_app("vendor/cache/ripe_gem-1.0.0.gem")).to exist + end + it "ignores cooldown and installs the locked version when frozen" do # Frozen installs read the lockfile instead of resolving, so cooldown has # no say. A version already locked inside the window must still install. From e86de2aa66bfea988659ed4a402a543e4c594053 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 16:27:48 +0900 Subject: [PATCH 12/14] [ruby/rubygems] Pin cross-platform cooldown exclusion in an e2e spec Exclusion is keyed on [name, version] and deliberately ignores platform, so pushing a fresh platform-specific build under an already-ripe version number cannot slip new code past the cooldown. No spec covered that behavior. https://github.com/ruby/rubygems/commit/be334bd252 Co-Authored-By: Claude Fable 5 --- spec/bundler/install/cooldown_spec.rb | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index e266d501923b37..1e7965ce0829a8 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -115,6 +115,19 @@ build_gem "fresh_gem", "0.3.2" do |s| s.date = now - (1 * 86_400) end + + # the generic build is outside the window, but a platform-specific + # build of the same version was pushed inside it + build_gem "late_platform", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "late_platform", "2.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "late_platform", "2.0.0" do |s| + s.platform = "x86_64-linux" + s.date = now - (1 * 86_400) + end end end @@ -712,6 +725,31 @@ expect(lockfile).not_to include("ripe_gem (2.0.0)") end + it "excludes a version on every platform when a platform-specific build of it is inside the window" do + # Exclusion is keyed on [name, version] and deliberately ignores + # platform: otherwise pushing a fresh platform-specific build under an + # already-ripe version number would slip new code past the cooldown. + gemfile <<-G + source "https://gem.repo3" + gem "late_platform" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("late_platform 1.0.0") + end + + it "selects the version with a late platform-specific build when --cooldown 0 bypasses the filter" do + gemfile <<-G + source "https://gem.repo3" + gem "late_platform" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("late_platform 2.0.0") + end + it "applies CLI --cooldown on bundle lock --update" do gemfile <<-G source "https://gem.repo3" From 0de0afb27ce884b38411154d8d5a45f5c6e86b0e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 17:36:25 +0900 Subject: [PATCH 13/14] [ruby/rubygems] Assert on the lockfile in the cooldown bypass control spec On x86_64-linux hosts `--cooldown 0` resolves to the platform-specific build, so `include_gems` failed expecting the ruby platform. https://github.com/ruby/rubygems/commit/d7be25160e Co-Authored-By: Claude Fable 5 --- spec/bundler/install/cooldown_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 1e7965ce0829a8..2724132263cd84 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -747,7 +747,10 @@ bundle "install --cooldown 0", artifice: "compact_index_cooldown" - expect(the_bundle).to include_gems("late_platform 2.0.0") + # On x86_64-linux hosts this resolves to the platform-specific build, so + # assert on the lockfile instead of the installed platform. + expect(lockfile).to include("late_platform (2.0.0") + expect(lockfile).not_to include("late_platform (1.0.0)") end it "applies CLI --cooldown on bundle lock --update" do From b4e40f781a749ca66289a21dc0936f6e076d0a89 Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Tue, 28 Jul 2026 18:21:57 -0700 Subject: [PATCH 14/14] [ruby/rubygems] Document Bundler checksum behavior for default gems Signed-off-by: Patrick Linnane https://github.com/ruby/rubygems/commit/56a82a2573 --- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-config.1.ronn | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index c055c8a415b060..32291e048c1f90 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -139,7 +139,7 @@ Cooldown filtering depends on the gem server providing a per\-version \fBcreated .IP "\(bu" 4 \fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. .IP "\(bu" 4 -\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. +\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. Bundler's own checksum is only included when its \fB\.gem\fR file is cached, which may not be the case when Bundler is installed as a default gem\. .IP "\(bu" 4 \fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \fR\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 72f891b428d58b..6b52288783b5c7 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -230,6 +230,8 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Gemfile to disable lockfile creation entirely (see gemfile(5)). * `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true. + Bundler's own checksum is only included when its `.gem` file is cached, which + may not be the case when Bundler is installed as a default gem. * `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`): Whether Bundler should skip building native extensions during installation. When set, gems are installed without compiling their C extensions.