From 5c92d3f99e3df82c6f24f42be48d29f64b9ca569 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Thu, 5 Mar 2026 02:31:42 +0100 Subject: [PATCH 01/11] [ruby/rubygems] Lock the checksum of Bundler itself in the lockfile: - ### Problem With the Bundler autoswitch feature, system Bundler may install a `bundler.gem` that matches the Gemfile.lock. The `bundler.gem` that gets downloaded is like any other gems, but its treated differently (it doesn't appear in the Gemfile specs and we also don't lock its checksum). If for any reason Bundler itself gets compromised, it's a security concern. ### Details I'd like to introduce this change into two separate changes for easier reviews. The first (this commit) only produce the checksum in the lockfile, nothings consumes it or verify it yet. The second patch will make sure that whenever the Bundler auto-install kicks in, Bundler will verify that the locked checksum matches the Bundler version being downloaded and installed. ### Solution Overall the solution here is similar to how checksums are already generated for other gems. However, the `bundler` gem comes from a different source (the `Bundler::Source::Metadata`) and so it needs to be handled slightly differently. A big part ot the change is test related. Instead of having to modify all tests that assert the state of the lockfile (which will be broken now, since the lockfile includes the Bundler checksum), I opted to automatically include the checksum whenever the helper metod `checksums_section` is called. https://github.com/ruby/rubygems/commit/9ce52a2188 --- lib/bundler/definition.rb | 2 ++ lib/bundler/lockfile_generator.rb | 15 ++++++++++++++- lib/bundler/lockfile_parser.rb | 9 ++++++++- lib/bundler/source/metadata.rb | 4 ++++ spec/bundler/commands/update_spec.rb | 3 +++ spec/bundler/support/checksums.rb | 4 ++-- 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index d9abc85d228f7d..69842b4813fa3e 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -988,6 +988,8 @@ def converge_sources end end + sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems + changes end diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb index 6b6cf9d9eaeea7..e23263048c76c4 100644 --- a/lib/bundler/lockfile_generator.rb +++ b/lib/bundler/lockfile_generator.rb @@ -71,7 +71,8 @@ def add_checksums checksums = definition.resolve.map do |spec| spec.source.checksum_store.to_lock(spec) end - add_section("CHECKSUMS", checksums) + + add_section("CHECKSUMS", checksums + bundler_checksum) end def add_locked_ruby_version @@ -100,5 +101,17 @@ def add_section(name, value) raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile" end end + + def bundler_checksum + return [] if Bundler.gem_version.to_s.end_with?(".dev") + + require "rubygems/package" + + bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last + package = Gem::Package.new(bundler_spec.cache_file) + definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package)) + + [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)] + end end end diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index ac0ce1ef3d0aaf..a837f994cd90da 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -28,6 +28,7 @@ def to_s attr_reader( :sources, + :metadata_source, :dependencies, :specs, :platforms, @@ -97,6 +98,7 @@ def self.bundled_with def initialize(lockfile, strict: false) @platforms = [] @sources = [] + @metadata_source = Source::Metadata.new @dependencies = {} @parse_method = nil @specs = {} @@ -252,7 +254,12 @@ def parse_checksum(line) version = Gem::Version.new(version) platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY full_name = Gem::NameTuple.new(name, version, platform).full_name - return unless spec = @specs[full_name] + spec = @specs[full_name] + + if name == "bundler" + spec ||= LazySpecification.new(name, version, platform, @metadata_source) + end + return unless spec if checksums checksums.split(",") do |lock_checksum| diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb index fd959cd64ee245..ecf889518715c6 100644 --- a/lib/bundler/source/metadata.rb +++ b/lib/bundler/source/metadata.rb @@ -58,6 +58,10 @@ def hash def version_message(spec) "#{spec.name} #{spec.version}" end + + def checksum_store + @checksum_store ||= Checksum::Store.new + end end end end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index cdaeb75c4a3399..3fc6bd38e41ab9 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1537,6 +1537,7 @@ checksums = checksums_section do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0") end install_gemfile <<-G @@ -1621,6 +1622,7 @@ checksums = checksums_section do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") end install_gemfile <<-G @@ -1745,6 +1747,7 @@ # Only updates properly on modern RubyGems. checksums = checksums_section_when_enabled do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") end expect(lockfile).to eq <<~L diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb index cf8ea417d67adb..03147bf6f4638f 100644 --- a/spec/bundler/support/checksums.rb +++ b/spec/bundler/support/checksums.rb @@ -14,9 +14,9 @@ def initialize_copy(original) @checksums = @checksums.dup end - def checksum(repo, name, version, platform = Gem::Platform::RUBY) + def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems") name_tuple = Gem::NameTuple.new(name, version, platform) - gem_file = File.join(repo, "gems", "#{name_tuple.full_name}.gem") + gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem") File.open(gem_file, "rb") do |f| register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)")) end From fc480c1cf0a8e308404ab51e9f61bae56a4c393a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 18 Mar 2026 09:14:35 -0700 Subject: [PATCH 02/11] ZJIT: Increase SYNTAX_SUGGEST_TIMEOUT for ZJIT CI from 5 to 30 (#16443) ZJIT CI runs with --zjit-call-threshold=1 which JIT-compiles every function on first call, adding significant overhead. The 5-second timeout for test-syntax-suggest's "does not timeout on massive files" test is too tight under this configuration, causing random failures on slow CI runners. YJIT CI is unaffected at 5 seconds. --- .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 e084855d3d6ef7..63c1da912ec883 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -125,7 +125,7 @@ jobs: RUBY_TESTOPTS: '-q --tty=no' EXCLUDES: '../src/test/.excludes-zjit' TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' - SYNTAX_SUGGEST_TIMEOUT: '5' + SYNTAX_SUGGEST_TIMEOUT: '30' PRECHECK_BUNDLED_GEMS: 'no' continue-on-error: ${{ matrix.continue-on-test_task || false }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 67b16a2ee82d31..be94206d7ae7da 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -182,7 +182,7 @@ jobs: EXCLUDES: '../src/test/.excludes-zjit' TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' PRECHECK_BUNDLED_GEMS: 'no' - SYNTAX_SUGGEST_TIMEOUT: '5' + SYNTAX_SUGGEST_TIMEOUT: '30' ZJIT_BINDGEN_DIFF_OPTS: '--exit-code' CLANG_PATH: ${{ matrix.clang_path }} continue-on-error: ${{ matrix.continue-on-test_task || false }} From 00a31a46f21ad49cb8a8b0e78aec43ec70a89a48 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 18 Mar 2026 09:19:25 -0700 Subject: [PATCH 03/11] ZJIT: Set up Launchable for ZJIT CI jobs (#16444) --- .github/actions/launchable/setup/action.yml | 12 ++++++++++++ .github/workflows/zjit-macos.yml | 18 ++++++++++++++++++ .github/workflows/zjit-ubuntu.yml | 18 ++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/.github/actions/launchable/setup/action.yml b/.github/actions/launchable/setup/action.yml index e0547633022da7..eb9ababd72f063 100644 --- a/.github/actions/launchable/setup/action.yml +++ b/.github/actions/launchable/setup/action.yml @@ -55,6 +55,12 @@ inputs: description: >- Whether this workflow is executed on YJIT. + is-zjit: + required: false + default: 'false' + description: >- + Whether this workflow is executed on ZJIT. + outputs: stdout_report_path: value: ${{ steps.global.outputs.stdout_report_path }} @@ -204,6 +210,11 @@ runs: btest_test_suite="yjit-${btest_test_suite}" test_spec_test_suite="yjit-${test_spec_test_suite}" fi + if [ "${INPUT_IS_ZJIT}" = "true" ]; then + test_all_test_suite="zjit-${test_all_test_suite}" + btest_test_suite="zjit-${btest_test_suite}" + test_spec_test_suite="zjit-${test_spec_test_suite}" + fi # launchable_setup target var -- refers ${target} prefixed variables launchable_setup() { local target=$1 session @@ -239,6 +250,7 @@ runs: INPUT_GITHUB_REF: ${{ github.ref }} INPUT_TEST_OPTS: ${{ inputs.test-opts }} INPUT_IS_YJIT: ${{ inputs.is-yjit }} + INPUT_IS_ZJIT: ${{ inputs.is-zjit }} INPUT_OS: ${{ inputs.os }} INPUT_TEST_TASK: ${{ inputs.test-task }} INPUT_WORKFLOW: ${{ github.workflow }} diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 63c1da912ec883..eb86dd649c28a6 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -113,8 +113,24 @@ jobs: run: | echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: macos-14 + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + is-zjit: true + continue-on-error: true + timeout-minutes: 3 + - name: make ${{ matrix.test_task }} run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + set -x make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ RUN_OPTS="$RUN_OPTS" \ @@ -127,6 +143,8 @@ jobs: TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' SYNTAX_SUGGEST_TIMEOUT: '30' PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} continue-on-error: ${{ matrix.continue-on-test_task || false }} - name: Dump crash logs diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index be94206d7ae7da..44227f913624ee 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -169,8 +169,24 @@ jobs: run: | echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: ${{ matrix.runs-on || 'ubuntu-22.04' }} + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + is-zjit: true + continue-on-error: true + timeout-minutes: 3 + - name: make ${{ matrix.test_task }} run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + set -x make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ RUN_OPTS="$RUN_OPTS" MSPECOPT=--debug SPECOPTS="$SPECOPTS" \ @@ -185,6 +201,8 @@ jobs: SYNTAX_SUGGEST_TIMEOUT: '30' ZJIT_BINDGEN_DIFF_OPTS: '--exit-code' CLANG_PATH: ${{ matrix.clang_path }} + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} continue-on-error: ${{ matrix.continue-on-test_task || false }} - name: Dump crash logs From 0636592ad2e60d2cd814c884490224cb1dc7d455 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 18 Mar 2026 08:56:17 +0100 Subject: [PATCH 04/11] [ruby/json] Fix a format string injection vulnerability In `JSON.parse(doc, allow_duplicate_key: false)`. https://github.com/ruby/json/commit/393b41c3e5 --- ext/json/parser/parser.c | 23 +++++++++++++++++++---- test/json/json_parser_test.rb | 7 +++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 42ca10894e8290..05cfaa7c685738 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -402,11 +402,9 @@ static void emit_parse_warning(const char *message, JSON_ParserState *state) #define PARSE_ERROR_FRAGMENT_LEN 32 -NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *state) +static VALUE build_parse_error_message(const char *format, JSON_ParserState *state, long line, long column) { unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 3]; - long line, column; - cursor_position(state, &line, &column); const char *ptr = "EOF"; if (state->cursor && state->cursor < state->end) { @@ -441,11 +439,23 @@ NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *st VALUE msg = rb_sprintf(format, ptr); VALUE message = rb_enc_sprintf(enc_utf8, "%s at line %ld column %ld", RSTRING_PTR(msg), line, column); RB_GC_GUARD(msg); + return message; +} +static VALUE parse_error_new(VALUE message, long line, long column) +{ VALUE exc = rb_exc_new_str(rb_path2class("JSON::ParserError"), message); rb_ivar_set(exc, rb_intern("@line"), LONG2NUM(line)); rb_ivar_set(exc, rb_intern("@column"), LONG2NUM(column)); - rb_exc_raise(exc); + return exc; +} + +NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *state) +{ + long line, column; + cursor_position(state, &line, &column); + VALUE message = build_parse_error_message(format, state, line, column); + rb_exc_raise(parse_error_new(message, line, column)); } NORETURN(static) void raise_parse_error_at(const char *format, JSON_ParserState *state, const char *at) @@ -895,6 +905,11 @@ NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE d rb_inspect(duplicate_key) ); + long line, column; + cursor_position(state, &line, &column); + rb_str_concat(message, build_parse_error_message("", state, line, column)) ; + rb_exc_raise(parse_error_new(message, line, column)); + raise_parse_error(RSTRING_PTR(message), state); RB_GC_GUARD(message); } diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 653abf46ce7cc8..2d2f065ecb8f2a 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -425,6 +425,13 @@ def test_parse_duplicate_key end end + def test_parse_duplicate_key_escape + error = assert_raise(ParserError) do + JSON.parse('{"%s%s%s%s":1,"%s%s%s%s":2}', allow_duplicate_key: false) + end + assert_match "%s%s%s%s", error.message + end + def test_some_wrong_inputs assert_raise(ParserError) { parse('[] bla') } assert_raise(ParserError) { parse('[] 1') } From e74e3e1120871023c85d1d1afd7070c6c3ebbc41 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 18 Mar 2026 18:23:53 +0100 Subject: [PATCH 05/11] [ruby/json] Release 2.19.2 https://github.com/ruby/json/commit/54f8a878ae --- ext/json/lib/json/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index aa56d3a4bf4017..8853ed885d2a42 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.19.1' + VERSION = '2.19.2' end From 896e5c489177b3420e7b109c1bc377149bfa0d2e Mon Sep 17 00:00:00 2001 From: git Date: Wed, 18 Mar 2026 17:27:10 +0000 Subject: [PATCH 06/11] Update default gems list at e74e3e1120871023c85d1d1afd7070 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index c1a0fc688b6529..59ac952461cda9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -64,7 +64,7 @@ releases. * RubyGems 4.1.0.dev * bundler 4.1.0.dev -* json 2.19.1 +* json 2.19.2 * 2.18.0 to [v2.18.1][json-v2.18.1], [v2.19.0][json-v2.19.0], [v2.19.1][json-v2.19.1] * openssl 4.0.1 * 4.0.0 to [v4.0.1][openssl-v4.0.1] From 35b9f9dcaa69f756b3aa97821020231333bed48d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 17 Mar 2026 15:47:26 -0400 Subject: [PATCH 07/11] ZJIT: Don't call write barrier if the object is an immediate In addition to compile-time knowledge, we can also (now that the global regalloc has landed) check at run time if the value being stored is a heap object. --- zjit/src/codegen.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 02bcd47ad3ddbc..0ff90ad103393a 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -668,7 +668,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::LoadSelf => gen_load_self(), &Insn::LoadField { recv, id, offset, return_type } => gen_load_field(asm, opnd!(recv), id, offset, return_type), &Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), function.type_of(val))), - &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(asm, opnd!(recv), opnd!(val), function.type_of(val))), + &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))), &Insn::IsBlockGiven { lep } => gen_is_block_given(asm, opnd!(lep)), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, opnds!(elements), opnd!(target), &function.frame_state(*state)), Insn::ArrayPackBuffer { elements, fmt, buffer, state } => gen_array_pack_buffer(jit, asm, opnds!(elements), opnd!(fmt), opnd!(buffer), &function.frame_state(*state)), @@ -1286,13 +1286,35 @@ fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: ID, offset: i32, val: Op asm.store(Opnd::mem(val_type.num_bits(), recv, offset), val); } -fn gen_write_barrier(asm: &mut Assembler, recv: Opnd, val: Opnd, val_type: Type) { +fn gen_write_barrier(jit: &mut JITState, asm: &mut Assembler, recv: Opnd, val: Opnd, val_type: Type) { // See RB_OBJ_WRITE/rb_obj_write: it's just assignment and rb_obj_written(). // rb_obj_written() does: if (!RB_SPECIAL_CONST_P(val)) { rb_gc_writebarrier(recv, val); } if !val_type.is_immediate() { asm_comment!(asm, "Write barrier"); let recv = asm.load(recv); + + // Create a result block that all paths converge to + let hir_block_id = asm.current_block().hir_block_id; + let rpo_idx = asm.current_block().rpo_index; + let result_block = asm.new_block(hir_block_id, false, rpo_idx); + let result_edge = Target::Block(lir::BranchEdge { target: result_block, args: vec![] }); + + // If non-false immediate, don't fire write barrier + asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64)); + asm.jnz(jit, result_edge.clone()); + + // If false, don't fire write barrier + asm.cmp(val, Qfalse.into()); + asm.je(jit, result_edge.clone()); + + // Heap object; fire the write barrier asm_ccall!(asm, rb_zjit_writebarrier_check_immediate, recv, val); + asm.jmp(result_edge); + + // Join block + asm.set_current_block(result_block); + let label = jit.get_label(asm, result_block, hir_block_id); + asm.write_label(label); } } From b2fc406ad3adbbf1d00e62126b80cd8a8c0b6fbc Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 17 Mar 2026 15:58:37 -0400 Subject: [PATCH 08/11] ZJIT: Remove rb_zjit_writebarrier_check_immediate --- zjit.c | 8 -------- zjit/bindgen/src/main.rs | 1 - zjit/src/codegen.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 1 - 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/zjit.c b/zjit.c index 68336858e958b1..1fdccbff64fab0 100644 --- a/zjit.c +++ b/zjit.c @@ -304,14 +304,6 @@ rb_zjit_class_has_default_allocator(VALUE klass) VALUE rb_vm_untag_block_handler(VALUE block_handler); VALUE rb_vm_get_untagged_block_handler(rb_control_frame_t *reg_cfp); -void -rb_zjit_writebarrier_check_immediate(VALUE recv, VALUE val) -{ - if (!RB_SPECIAL_CONST_P(val)) { - rb_gc_writebarrier(recv, val); - } -} - // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_enable(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 7144e73737ba03..fa61f61481fd95 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -146,7 +146,6 @@ fn main() { .allowlist_function("rb_gc_writebarrier") .allowlist_function("rb_gc_writebarrier_remember") .allowlist_function("rb_gc_register_mark_object") - .allowlist_function("rb_zjit_writebarrier_check_immediate") // VALUE variables for Ruby class objects .allowlist_var("rb_cBasicObject") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 0ff90ad103393a..0dd35bdf7eaaac 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1308,7 +1308,7 @@ fn gen_write_barrier(jit: &mut JITState, asm: &mut Assembler, recv: Opnd, val: O asm.je(jit, result_edge.clone()); // Heap object; fire the write barrier - asm_ccall!(asm, rb_zjit_writebarrier_check_immediate, recv, val); + asm_ccall!(asm, rb_gc_writebarrier, recv, val); asm.jmp(result_edge); // Join block diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index fd3b79684ccff6..2b643d22ddd471 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2167,7 +2167,6 @@ unsafe extern "C" { pub fn rb_zjit_class_has_default_allocator(klass: VALUE) -> bool; pub fn rb_vm_untag_block_handler(block_handler: VALUE) -> VALUE; pub fn rb_vm_get_untagged_block_handler(reg_cfp: *mut rb_control_frame_t) -> VALUE; - pub fn rb_zjit_writebarrier_check_immediate(recv: VALUE, val: VALUE); pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; From 077871cf5b734b1f5a41edc23f0487b2b2ff6848 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 18 Mar 2026 23:09:46 +0100 Subject: [PATCH 09/11] [ruby/rubygems] [DOC] Fix link https://github.com/ruby/rubygems/commit/432a0f7e61 --- lib/rubygems.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 75d04efbe75e35..dbdbd1f5c38d53 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -38,7 +38,7 @@ module Gem # Further RubyGems documentation can be found at: # # * {RubyGems Guides}[https://guides.rubygems.org] -# * {RubyGems API}[https://www.rubydoc.info/github/ruby/rubygems] (also available from +# * {RubyGems API}[https://guides.rubygems.org/rubygems-org-api/] (also available from # gem server) # # == RubyGems Plugins From f0f1f02dac844124e479883b6efda6b5295cf570 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 18 Mar 2026 15:51:10 -0700 Subject: [PATCH 10/11] ZJIT: Remove stale libminiruby.a before rebuilding (#16439) `ar` with replace mode preserves old archive members that are no longer in the input list. When object files like prism/node.o get recompiled with different symbols (e.g. after prism updates), the stale version in the archive can cause undefined reference errors during zjit-test linking. Delete the archive first to ensure it only contains current objects. --- zjit/zjit.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/zjit.mk b/zjit/zjit.mk index 92839cf0a141f6..eecb6fdecf03d1 100644 --- a/zjit/zjit.mk +++ b/zjit/zjit.mk @@ -132,6 +132,7 @@ zjit-test-rr: libminiruby.a # - Less likely to break since later stages of the build process also rely on miniruby. libminiruby.a: miniruby$(EXEEXT) $(ECHO) linking static-library $@ + $(Q) $(RM) $@ $(Q) $(AR) $(ARFLAGS) $@ $(MINIOBJS) $(COMMONOBJS) libminiruby: libminiruby.a From ab32c0e690b805cdaaf264ad4c3421696c588204 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 18 Mar 2026 17:41:25 -0700 Subject: [PATCH 11/11] Allow reading cvars from non-main Ractors (#16308) Today you can read instance variables from non-main Ractors, but many Rails applications use cvars, and we cannot read them. For example: ```ruby class Foo # This is NOT allowed to be read in non-main Ractors @@bar = 123 def self.bar; @@bar; end # This is allowed to be read in non-main Ractors @baz = 123 def self.baz; @baz; end end # This is OK Ractor.new { p Foo.baz }.value # Exception here Ractor.new { p Foo.bar }.value ``` This commit changes the semantics of cvars to be the same as instance variables: * It's ok to read Ractor shareable objects from the non-main Ractor * It's NOT ok to write from the non-main Ractor [Feature #21942] --- bootstraptest/test_ractor.rb | 97 ++++++++++++++++++++++++++++++++++-- variable.c | 16 +++++- 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 08f9b73c13192b..040943a0b94fc8 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1021,8 +1021,8 @@ def initialize values.join } -# cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors (@@cv from C)', %q{ +# Reading non-shareable cvar from non-main Ractor is not allowed +assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{ class C @@cv = 'str' end @@ -1040,8 +1040,8 @@ class C end } -# also cached cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors (@@cv from C)', %q{ +# also cached non-shareable cvar read from non-main Ractor is not allowed +assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{ class C @@cv = 'str' def self.cv @@ -1062,6 +1062,95 @@ def self.cv end } +# Reading shareable cvar from non-main Ractor is allowed +assert_equal 'shareable', %q{ + class C + @@cv = 'shareable'.freeze + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# Reading shareable cvar (integer) from non-main Ractor is allowed +assert_equal '42', %q{ + class C + @@cv = 42 + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value.to_s +} + +# Reading shareable cvar via module include from non-main Ractor is allowed +assert_equal 'hello', %q{ + module M + @@cv = 'hello'.freeze + def self.cv + @@cv + end + end + + class C + include M + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# Writing cvar from non-main Ractor is not allowed +assert_equal 'can not set class variables from non-main Ractors (@@cv from C)', %q{ + class C + @@cv = 'str' + def self.cv=(v) + @@cv = v + end + end + + r = Ractor.new do + C.cv = 'new' + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Reading cvar that was made shareable after initial assignment +assert_equal 'made shareable', %q{ + class C + @@cv = +'made shareable' + Ractor.make_shareable(@@cv) + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# cvar_defined? works from non-main Ractor +assert_equal 'true', %q{ + class C + @@cv = 42 + def self.cv? + defined?(@@cv) + end + end + + r = Ractor.new { C.cv? ? 'true' : 'false' } + r.value +} + # Getting non-shareable objects via constants by other Ractors is not allowed assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', <<~'RUBY', frozen_string_literal: false class C diff --git a/variable.c b/variable.c index 3576ff00041b47..9d0e4e4a2b9eac 100644 --- a/variable.c +++ b/variable.c @@ -1199,7 +1199,17 @@ static void CVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(VALUE klass, ID id) { if (UNLIKELY(!rb_ractor_main_p())) { - rb_raise(rb_eRactorIsolationError, "can not access class variables from non-main Ractors (%"PRIsVALUE" from %"PRIsVALUE")", rb_id2str(id), klass); + rb_raise(rb_eRactorIsolationError, "can not set class variables from non-main Ractors (%"PRIsVALUE" from %"PRIsVALUE")", rb_id2str(id), klass); + } +} + +static void +cvar_read_ractor_check(VALUE klass, ID id, VALUE val) +{ + if (UNLIKELY(!rb_ractor_main_p()) && !rb_ractor_shareable_p(val)) { + rb_raise(rb_eRactorIsolationError, + "can not read non-shareable class variable %"PRIsVALUE" from non-main Ractors (%"PRIsVALUE")", + rb_id2str(id), klass); } } @@ -4218,7 +4228,6 @@ cvar_overtaken(VALUE front, VALUE target, ID id) } #define CVAR_LOOKUP(v,r) do {\ - CVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(klass, id); \ if (cvar_lookup_at(klass, id, (v))) {r;}\ CVAR_FOREACH_ANCESTORS(klass, v, r);\ } while(0) @@ -4254,6 +4263,8 @@ check_for_cvar_table(VALUE subclass, VALUE key) void rb_cvar_set(VALUE klass, ID id, VALUE val) { + CVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(klass, id); + VALUE tmp, front = 0, target = 0; tmp = klass; @@ -4319,6 +4330,7 @@ rb_cvar_find(VALUE klass, ID id, VALUE *front) klass, ID2SYM(id)); } cvar_overtaken(*front, target, id); + cvar_read_ractor_check(klass, id, value); return (VALUE)value; }